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/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..bc0588a8404 --- /dev/null +++ b/configadmin/changelog.txt @@ -0,0 +1,291 @@ +Changes in 1.9.6 +---------------- +** Bug + * [FELIX-5908] : NoClassDefFoundError for the CM Security Domain combiner + + +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..55ef946b3f5 --- /dev/null +++ b/configadmin/pom.xml @@ -0,0 +1,315 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 5 + + + + org.apache.felix.configadmin + 1.9.5-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.7.0-SNAPSHOT + 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..9dd39a2e844 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/PersistenceManager.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.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. + */ + 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..ae41e4c9718 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/file/FilePersistenceManager.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.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#CM_CONFIG_DIR} bundle + * context property. That is the Configuration Admin Service creates an instance + * of this class calling + * new FilePersistenceManager(bundleContext, bundleContext.getProperty(CM_CONFIG_DIR)). + *

      + * 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
      samplesample.config
      org.apache.felix.log.LogServiceorg/apache/felix/log/LogService.config
      sample.flächesample/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..11cce0f43f7 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/Activator.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.cm.impl; + +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.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 not specified, the old behaviour is used + * and all available pms are used + * + * @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; + } + } +} + 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..25e4c711575 --- /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( this.getBundle().getLocation(), 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( getBundle().getLocation(), 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, + this.getBundle().getLocation() } ); + + // FELIX-3360: first implicit binding is dynamic + config.setDynamicBundleLocation( getBundle().getLocation(), 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( getBundle().getLocation() ) ) ) + { + 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, getBundle().getLocation() } ); + } + 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, getBundle().getLocation(), 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, getBundle().getLocation() } ); + + } + } + 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, getBundle().getLocation() } ); + } + } + + + /** + * 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( getBundle().getLocation(), 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, + this.getBundle().getLocation() } ); + + // FELIX-3360: first implicit binding is dynamic + config.setDynamicBundleLocation( getBundle().getLocation(), 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..ed72b2291f8 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationImpl.java @@ -0,0 +1,898 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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:"; + + /** + * 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 transient + * and not persisted. Thus it is restarted from zero each time + * an instance of this class is created. + */ + 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 ); + } + + 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 + 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 ); + 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 + 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 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++; + } + } + + + 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 ); + } + + + 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 ); + } + + + private static final String[] AUTO_PROPS = new String[] { + Constants.SERVICE_PID, + ConfigurationAdmin.SERVICE_FACTORYPID, + ConfigurationAdmin.SERVICE_BUNDLELOCATION, + PROPERTY_LOCKED + }; + + 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..4627d6b035d --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationManager.java @@ -0,0 +1,1687 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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" ); + 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( serviceBundle.getLocation() ); + 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( serviceBundle.getLocation() ); + 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, configurationAdmin.getBundle().getLocation(), + 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 = event.getBundle().getLocation(); + + // 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( serviceBundle.getLocation() ); + 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[] + { bundle.getLocation() } ); + 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 ), bundle.getLocation(), location } ); + return hasPermission; + } + + Log.logger.log( LogService.LOG_DEBUG, "canReceive=true; bundle={0}; configuration={1} (no SecurityManager)", + new Object[] + { bundle.getLocation(), location } ); + return true; + } + else + { + // single location, must match + final boolean hasPermission = location.equals( bundle.getLocation() ); + Log.logger.log( LogService.LOG_DEBUG, "canReceive={0}: bundle={1}; configuration={2}", new Object[] + { new Boolean( hasPermission ), bundle.getLocation(), 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( serviceBundle.getLocation() ); + + // 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( bundle.getLocation() ); + + 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( srBundle.getLocation() ); + } + + 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() + { + if ( event == null ) + { + this.event = new ConfigurationEvent( getServiceReference(), 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]} ); + + try + { + listeners[serviceIndex].configurationEvent( getConfigurationEvent() ); + } + 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..739af667776 --- /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 ) + { + 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..d32ce9c82e0 --- /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( bundles[i].getLocation() ); + } + + // 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..13cdff236f0 --- /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( provider.getLocation() ); + } + 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..4533f0832a1 --- /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; + } + + + 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..a128c1d04e6 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/TargetedPID.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.cm.impl.helper; + + +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( serviceBundle.getLocation() ); + } + + + /** + * 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..4b63419c3f9 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/PersistenceManagerTracker.java @@ -0,0 +1,353 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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(); + } + activate(holder.getPersistenceManager()); + } + }); + } + } + 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.compareTo(holder) != 0 ) + { + this.workerQueue.enqueue(new Runnable() + { + + @Override + public void run() + { + deactivate(); + activate(holder.getPersistenceManager()); + } + }); + } + } + + } + + + @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() ) + { + activate(holders.get(0).getPersistenceManager()); + } + } + }); + } + } + } + + public static final class Holder implements Comparable + { + private final ServiceReference reference; + + private final ExtPersistenceManager manager; + + 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); + } + + @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..70d148358f6 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/MockNotCachablePersistenceManager.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.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 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, 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..98e2f9db3a8 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/impl/ConfigurationManagerTest.java @@ -0,0 +1,533 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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 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..2e8a3882be0 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigAdminSecurityTest.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.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.ManagedServiceFactoryTestActivator3; +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.7.0-SNAPSHOT" ) + ); + } + + @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 ); + } +} 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/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/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..4fa6ebf7c7e --- /dev/null +++ b/configurator/pom.xml @@ -0,0 +1,157 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 5 + + + + org.apache.felix.configurator + bundle + + Apache Felix Configurator Service + + Implementation of the OSGi Configurator Service Specification 1.0 + + 1.0.5-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..69d6c3df022 --- /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 { + StringBuilder builder = new StringBuilder(); + MessageDigest md = MessageDigest.getInstance("SHA-256"); + for (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..fd54bfaaf5f --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/model/AbstractState.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.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 final 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 + */ + 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()); + } + ReflectionUtil.setField(this, "configurationsByPid", 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..0b2b03fc514 --- /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 final String pid; + + /** The configuration ranking */ + private final int ranking; + + /** The bundle id. */ + private final long bundleId; + + /** The configuration policy. */ + private final ConfigPolicy policy; + + /** The configuration properties. */ + private final 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()); + } + ReflectionUtil.setField(this, "pid", in.readObject()); + ReflectionUtil.setField(this, "properties", in.readObject()); + ReflectionUtil.setField(this, "policy", ConfigPolicy.valueOf((String)in.readObject())); + ReflectionUtil.setField(this, "bundleId", in.readLong()); + ReflectionUtil.setField(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..94df3a92f0a --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/model/ConfigList.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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 final 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 + */ + 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()); + } + ReflectionUtil.setField(this, "configurations", 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) { + this.hasChanges = true; + for(final Config cfg : configs) { + // search if we already have this configuration + for(final Config current : this.configurations) { + if ( current.getBundleId() == cfg.getBundleId() + && current.getRanking() == cfg.getRanking()) { + if ( current.getState() == ConfigState.UNINSTALL ) { + cfg.setState(ConfigState.INSTALLED); + current.setState(ConfigState.UNINSTALLED); + } + break; + } + } + } + this.configurations.addAll(configs.configurations); + 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/ReflectionUtil.java b/configurator/src/main/java/org/apache/felix/configurator/impl/model/ReflectionUtil.java new file mode 100644 index 00000000000..8732fcf8918 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/model/ReflectionUtil.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.configurator.impl.model; + +import java.io.IOException; +import java.lang.reflect.Field; + +public class ReflectionUtil { + + /** + * Set a (final) field during deserialization. + */ + public static void setField(final Object obj, final String name, final Object value) + throws IOException { + Class clazz = obj.getClass(); + while ( clazz != null ) { + try { + final Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + field.set(obj, value); + return; + } catch (final NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } catch (final SecurityException | IllegalArgumentException | IllegalAccessException e) { + throw (IOException)new IOException().initCause(e); + } + } + throw new IOException("Field " + name + " not found in class " + obj.getClass()); + } + +} 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..c509295c980 --- /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 final Map bundlesLastModified = new HashMap<>(); + + private final 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()); + } + ReflectionUtil.setField(this, "bundlesLastModified", in.readObject()); + ReflectionUtil.setField(this, "bundlesConfigAdminBundleId", 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/main/resources/OSGI-INF/permissions.perm b/configurator/src/main/resources/OSGI-INF/permissions.perm new file mode 100644 index 00000000000..e8f4636156a --- /dev/null +++ b/configurator/src/main/resources/OSGI-INF/permissions.perm @@ -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. + +# Imported packages +# -> MANIFEST.MF +(org.osgi.framework.PackagePermission "org.osgi.framework" "import") +(org.osgi.framework.PackagePermission "org.osgi.framework.wiring" "import") +(org.osgi.framework.PackagePermission "org.osgi.util.tracker" "import") +(org.osgi.framework.PackagePermission "org.osgi.service.cm" "import") +(org.osgi.framework.PackagePermission "org.osgi.service.log" "import") +(org.osgi.framework.PackagePermission "org.osgi.service.coordinator" "import") + +# General bundle permissions +(java.util.PropertyPermission "configurator.*" "read") +(org.osgi.framework.ServicePermission "org.osgi.service.cm.ConfigurationAdmin" "get") +(org.osgi.framework.ServicePermission "org.osgi.service.coordinator.Coordinator" "get") +(org.osgi.framework.ServicePermission "org.osgi.service.log.LogService" "get") + +# Permission to provide osgi.extender capability +(org.osgi.framework.CapabilityPermission "osgi.extender" "provide") + +# Permission to adapt Bundle to BundleWiring +(org.osgi.framework.AdaptPermission "(adaptClass=org.osgi.framework.wiring.BundleWiring)" "adapt") + +# We need permissions for ourselves (e.g. to add a BundleListener) +(org.osgi.framework.AdminPermission "(name=org.apache.felix.configurator)" "resource,metadata,class,context,listener") + +# We need access to the resources, context and metadata of other bundles +# to process configuration files and apply the config on their behalve +(org.osgi.framework.AdminPermission "(name=*)" "metadata,context,resource") + +# Johnzon needs this to read properties +(java.util.PropertyPermission "org.apache.johnzon.*" "read") + +# Embedded Converter needs this to check whether something is a DTO +(java.lang.RuntimePermission "accessDeclaredMembers") + +# Manage configurations +(org.osgi.service.cm.ConfigurationPermission "*" "configure") + +# Handle binaries +(java.io.FilePermission "-" "read,write,execute,delete") + 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..c18c1b86c19 --- /dev/null +++ b/configurator/src/test/java/org/apache/felix/configurator/impl/ConfiguratorTest.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.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 { + final Bundle b = mock(Bundle.class); + when(b.getBundleId()).thenReturn(id); + when(b.getLastModified()).thenReturn(5L); + 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/" + id + ".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 testSimpleRankingRemove() throws Exception { + final Bundle b1 = setupBundle(1); + final Bundle b2 = setupBundle(2); + + 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(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}); + + 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..9079ef8ef32 --- /dev/null +++ b/configurator/src/test/resources/bundles/2.json @@ -0,0 +1,8 @@ +{ + "a" : { + "foo" : "bar2" + }, + "b" : { + "x" : "y2" + } +} \ 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..0bc11a2a697 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/PojoSR.java @@ -0,0 +1,695 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.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) + { + startBundles(scan); + } + } + + public void startBundles(Collection scan) throws Exception + { + for (BundleDescriptor desc : scan) + { + 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); + } + + + for (Bundle bundle : m_bundles.values()) + { + try + { + bundle.start(); + } + catch (Throwable e) + { + System.out.println("Unable to start bundle: " + bundle); + e.printStackTrace(); + } + } + + } + + 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..a63421b68e2 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/launch/PojoServiceRegistry.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.connect.launch; + +import java.util.Collection; +import java.util.Dictionary; + +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 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..2ce9523014a --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/launch/PojoServiceRegistryFactory.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.connect.launch; + +import java.util.Map; + +public interface PojoServiceRegistryFactory +{ + public static final String BUNDLE_DESCRIPTORS = + PojoServiceRegistry.class.getName().toLowerCase() + ".bundles"; + + 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..10bd3b90e9b --- /dev/null +++ b/converter/converter/pom.xml @@ -0,0 +1,139 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 4 + ../pom/pom.xml + + + Apache Felix Converter + org.apache.felix.converter + 1.0.1-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 + 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..a5888d3c6b3 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/ConvertingImpl.java @@ -0,0 +1,1281 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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.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); + } + + volatile 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; + } + + @Override + public void setConverter(Converter c) { + if (c instanceof InternalConverter) + converter = (InternalConverter) c; + else + throw new IllegalStateException( + "Incorrect converter used. Should implement " + + InternalConverter.class + " but was " + c); + } + + @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) { + 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); + + 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(); + if (res != null) + return res; + + if (targetAsClass.isArray()) { + return convertToArray(targetAsClass.getComponentType(), + targetAsClass.getComponentType()); + } else if (type instanceof GenericArrayType) { + return convertToArray(targetAsClass, + ((GenericArrayType) type).getGenericComponentType()); + } else if (Collection.class.isAssignableFrom(targetAsClass)) { + return convertToCollectionType(); + } else if (isMapType(targetAsClass, targetAsJavaBean, targetAsDTO)) { + return convertToMapType(); + } + + // 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); + } else if (isMapType(sourceClass, sourceAsJavaBean, sourceAsDTO)) { + return convertMapToSingleValue(targetAsClass); + } else if (object instanceof Map.Entry) { + return convertMapEntryToSingleValue(targetAsClass); + } else if ((object = asBoxedArray(object)) instanceof Object[]) { + return convertArrayToSingleValue(targetAsClass); + } + + Object res2 = tryStandardMethods(); + if (res2 != null) { + return res2; + } else { + if (hasDefault) + return converter.convert(defaultValue) + .sourceAs(sourceAsClass) + .targetAs(targetAsClass) + .to(targetClass); + else + throw new ConversionException( + "Cannot convert " + object + " to " + targetAsClass); + } + } + + private Object convertArrayToSingleValue(Class< ? > cls) { + Object[] arr = (Object[]) object; + if (arr.length == 0) + return null; + else + return converter.convert(arr[0]).to(cls); + } + + private Object convertCollectionToSingleValue(Class< ? > cls) { + Collection< ? > coll = (Collection< ? >) object; + if (coll.size() == 0) + return null; + else + return converter.convert(coll.iterator().next()).to(cls); + } + + private Object convertMapToSingleValue(Class< ? > cls) { + Map< ? , ? > m = mapView(object, sourceClass, converter); + if (m.size() > 0) { + return converter.convert(m.entrySet().iterator().next()).to(cls); + } else { + return null; + } + } + + @SuppressWarnings("rawtypes") + private Object convertMapEntryToSingleValue(Class< ? > cls) { + 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 converter.convert(entry.getKey()).to(cls); + } else if (cls.equals(valueCls)) { + return converter.convert(entry.getValue()).to(cls); + } else if (cls.isAssignableFrom(keyCls)) { + return converter.convert(entry.getKey()).to(cls); + } else if (cls.isAssignableFrom(valueCls)) { + return converter.convert(entry.getValue()).to(cls); + } else if (entry.getKey() instanceof String) { + return converter.convert(entry.getKey()).to(cls); + } else if (entry.getValue() instanceof String) { + return converter.convert(entry.getValue()).to(cls); + } + + return converter + .convert(converter.convert(entry.getKey()).to(String.class)) + .to(cls); + } + + @SuppressWarnings("unchecked") + private T convertToArray(Class< ? > componentClz, Type componentType) { + Collection< ? > collectionView = collectionView(); + 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 = converter.convert(next) + .to(componentType); + Array.set(array, i, converted); + } + return (T) array; + } catch (Exception e) { + return null; + } + } + + @SuppressWarnings("unchecked") + private T convertToCollectionType() { + Collection< ? > res = convertToCollectionDelegate(); + if (res != null) + return (T) res; + + return convertToCollection(); + } + + private Collection< ? > convertToCollectionDelegate() { + if (!liveView) + return null; + + if (List.class.equals(targetClass) + || Collection.class.equals(targetClass)) { + if (sourceClass.isArray()) { + return ListDelegate.forArray(object, this); + } else if (Collection.class.isAssignableFrom(sourceClass)) { + return ListDelegate.forCollection((Collection< ? >) object, + this); + } + } else if (Set.class.equals(targetClass)) { + if (sourceClass.isArray()) { + return SetDelegate.forCollection( + ListDelegate.forArray(object, this), this); + } else if (Collection.class.isAssignableFrom(sourceClass)) { + return SetDelegate.forCollection((Collection< ? >) object, + this); + } + } + return null; + } + + @SuppressWarnings({ + "rawtypes", "unchecked" + }) + private T convertToCollection() { + Collection< ? > cv = collectionView(); + 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 = converter.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) { + Map m = mapView(object, sourceCls, converter); + + 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.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + try { + f = targetAsCls.getField(fieldName); + } catch (NoSuchFieldException | NullPointerException e1) { + // There is no field with this name + if (keysIgnoreCase) { + // If enabled, try again but now ignore case + for (Field fs : targetAsCls.getDeclaredFields()) { + 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())) + val = converter.convert(val).sourceAsDTO().to( + f.getType()); + else { + Type genericType = reifyType(f.getGenericType(), + targetAsClass, typeArguments); + val = converter.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.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers)) + continue; + if (!Modifier.isPublic(modifiers)) + continue; + + String name = field.getName(); + if (!names.contains(name)) + names.add(name); + + } + return names; + } + + @SuppressWarnings({ + "rawtypes", "unchecked" + }) + private Map convertToMap() { + Map m = mapView(object, sourceClass, converter); + 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); + value = convertMapValue(value); + instance.put(key, value); + } + + return instance; + } + + Object convertCollectionValue(Object element) { + Type type = null; + if (typeArguments != null && typeArguments.length > 0) { + type = typeArguments[0]; + } + + if (element != null) { + if (type != null) { + element = converter.convert(element).to(type); + } else { + Class< ? > cls = element.getClass(); + if (isCopyRequiredType(cls)) { + cls = getConstructableType(cls); + } + + if (sourceAsDTO || DTOUtil.isDTOType(cls)) + element = converter.convert(element).sourceAsDTO().to(cls); + else + element = converter.convert(element).to(cls); + } + } + return element; + } + + Object convertMapKey(Object key) { + return convertMapElement(key, 0); + } + + Object convertMapValue(Object value) { + return convertMapElement(value, 1); + } + + private Object convertMapElement(Object element, int typeIdx) { + Type type = null; + if (typeArguments != null && typeArguments.length > typeIdx) { + type = typeArguments[typeIdx]; + } + + if (element != null) { + if (type != null) { + element = converter.convert(element).to(type); + } else { + Class< ? > cls = element.getClass(); + if (isCopyRequiredType(cls)) { + cls = getConstructableType(cls); + } + + if (sourceAsDTO || DTOUtil.isDTOType(cls)) + element = converter.convert(element).sourceAsDTO().to(cls); + else + element = converter.convert(element).to(cls); + } + } + return element; + } + + @SuppressWarnings({ + "unchecked", "rawtypes" + }) + private Map convertToMapDelegate() { + if (Map.class.isAssignableFrom(sourceClass)) { + return MapDelegate.forMap((Map) object, this); + } else if (Dictionary.class.isAssignableFrom(sourceClass)) { + return MapDelegate.forDictionary((Dictionary) object, this); + } else if (DTOUtil.isDTOType(sourceClass) || sourceAsDTO) { + return MapDelegate.forDTO(object, sourceClass, this); + } else if (sourceAsJavaBean) { + return MapDelegate.forBean(object, sourceClass, this); + } 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); + } + return null; + } + + @SuppressWarnings("rawtypes") + private Object convertToMapType() { + if (!isMapType(sourceClass, sourceAsJavaBean, sourceAsDTO)) { + throw new ConversionException( + "Cannot convert " + object + " to " + targetAsClass); + } + + if (Map.class.equals(targetClass) && liveView) { + Map res = convertToMapDelegate(); + if (res != null) + return res; + } + + if (Map.class.isAssignableFrom(targetAsClass)) + return convertToMap(); + else if (Dictionary.class.isAssignableFrom(targetAsClass)) + return convertToDictionary(); + else if (targetAsDTO || DTOUtil.isDTOType(targetAsClass)) + return convertToDTO(sourceClass, targetAsClass); + else if (targetAsClass.isInterface()) + return convertToInterface(sourceClass, targetAsClass); + else if (targetAsJavaBean) + return convertToJavaBean(sourceClass, targetAsClass); + throw new ConversionException( + "Cannot convert " + object + " to " + targetAsClass); + } + + @SuppressWarnings({ + "unchecked", "rawtypes" + }) + private Object convertToDictionary() { + return new Hashtable( + (Map) converter.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) { + String prefix = Util.getPrefix(targetCls); + + @SuppressWarnings("rawtypes") + Map m = mapView(object, sourceCls, converter); + 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, converter.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) { + InternalConverting ic = converter.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); + } + + private Object createProxy(final Class< ? > cls, final Map< ? , ? > data) { + 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 converter.convert(val).to(genericType); + } + }); + } + + @SuppressWarnings("boxing") + private Object handleNull(Class< ? > cls) { + if (hasDefault) + return converter.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 converter.convert(Collections.emptyList()).to(cls); + } + // This is not a primitive, just return null + return null; + } + + return converter.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)) + return true; + else if (asJavaBean && isWriteableJavaBean(cls)) + return true; + else + return Dictionary.class.isAssignableFrom(cls); + } + + @SuppressWarnings("boxing") + private Object trySpecialCases() { + 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 + Class< ? > ann = Util.getAnnotationType(sourceClass, object); + String key = Util.toSingleElementAnnotationKey(ann.getSimpleName()); + return converter + .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()); + } else { + throw new ConversionException("Cannot convert " + object + + " to marker annotation " + targetAsClass); + } + } + return null; + } + + private boolean isMarkerAnnotation(Class< ? > annClass) { + for (Method m : annClass.getDeclaredMethods()) { + try { + if (Annotation.class + .getMethod(m.getName(), m.getParameterTypes()) + .getReturnType() + .equals(m.getReturnType())) + // this is a base annotation method + continue; + } catch (Exception ex) { + // Method not found, not a marker annotation + } + return false; + } + return true; + } + + @SuppressWarnings("unchecked") + private T tryStandardMethods() { + try { + Method m = targetAsClass.getDeclaredMethod("valueOf", String.class); + if (m != null) { + 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() { + if (object == null) + return null; + + Collection< ? > c = asCollection(); + if (c == null) + return Collections.singleton(object); + else + return c; + } + + private Collection< ? > asCollection() { + 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, converter).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(); + for (Method md : sourceCls.getDeclaredMethods()) { + handleBeanMethod(obj, md, invokedMethods, result); + } + + return result; + } + + @SuppressWarnings("rawtypes") + private Map createMapFromDTO(Object obj, InternalConverter ic) { + Set handledFields = new HashSet<>(); + + Map result = new HashMap(); + // Do we need 'declaredfields'? We only need to look at the public + // ones... + for (Field f : obj.getClass().getDeclaredFields()) { + handleDTOField(obj, f, handledFields, result, ic); + } + for (Field f : obj.getClass().getFields()) { + handleDTOField(obj, f, handledFields, result, ic); + } + return result; + } + + @SuppressWarnings("rawtypes") + private static Map createMapFromInterface(Object obj, Class< ? > srcCls) { + Map result = new HashMap(); + + 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); + for (Iterator> it = interfaces.iterator(); it.hasNext();) { + Class< ? > intf = it.next(); + if (intf.getDeclaredMethods().length == 0) + 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); + } else { + classes.addAll(Arrays.asList(cls.getInterfaces())); + } + + 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) && obj instanceof Map)) + return (Map< ? , ? >) obj; + else if (Dictionary.class.isAssignableFrom(sourceCls)) + return MapDelegate.forDictionary((Dictionary< ? , ? >) object, + this); + else if (DTOUtil.isDTOType(sourceCls) || 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); + } + return createMapFromInterface(obj, sourceClass); + } + + private boolean hasGetProperties(Class< ? > cls) { + try { + Method m = cls.getDeclaredMethod("getProperties"); + if (m == null) + m = cls.getMethod("getProperties"); + return m != null; + } catch (Exception e) { + return false; + } + } + + private Map< ? , ? > getPropertiesDelegate(Object obj, Class< ? > cls) { + try { + Method m = cls.getDeclaredMethod("getProperties"); + if (m == null) + m = cls.getMethod("getProperties"); + + return converter.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) || 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<>(); + methods.addAll(Arrays.asList(cls.getDeclaredMethods())); + 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..8e1101cb35f --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/CustomConverterImpl.java @@ -0,0 +1,202 @@ +/* + * 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 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); + converting.setConverter(this); + return new ConvertingWrapper(obj, converting); + } + + @Override + public Functioning function() { + return new FunctioningImpl(this); + } + + @Override + public ConverterBuilder newConverterBuilder() { + return new ConverterBuilderImpl(this); + } + + private class ConvertingWrapper implements InternalConverting { + private final InternalConverting del; + private final Object object; + private volatile Object defaultValue; + private volatile boolean hasDefault; + + ConvertingWrapper(Object obj, InternalConverting c) { + 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 void setConverter(Converter c) { + del.setConverter(c); + } + + @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) { + 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); + } + } + } + + return del.to(type); + } catch (Exception ex) { + for (ConverterFunction eh : errorHandlers) { + 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; + } + } + + @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..50017beee35 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/DTOUtil.java @@ -0,0 +1,76 @@ +/* + * 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) { + try { + cls.getDeclaredConstructor(); + } catch (NoSuchMethodException | SecurityException e) { + // No zero-arg constructor, not a DTO + return false; + } + + if (cls.getDeclaredMethods().length > 0) { + // should not have any methods + 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..d3f265e0d32 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/InternalConverting.java @@ -0,0 +1,34 @@ +/* + * 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 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 { + /** + * Set the current converter. + * + * @param c The current converter. + */ + void setConverter(Converter 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..c63d02ef15d --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/ListDelegate.java @@ -0,0 +1,247 @@ +/* + * 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; + + @SuppressWarnings({ + "unchecked", "rawtypes" + }) + static List forArray(Object arr, ConvertingImpl converting) { + return new ListDelegate(new ArrayDelegate(arr), converting); + } + + static List forCollection(Collection object, + ConvertingImpl converting) { + if (object instanceof List) { + return new ListDelegate((List) object, converting); + } + return new ListDelegate(new CollectionDelegate<>(object), + converting); + } + + private ListDelegate(List del, ConvertingImpl conv) { + delegate = del; + convertingImpl = conv; + } + + // 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)); + } + + @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..0c865883a04 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/MapDelegate.java @@ -0,0 +1,251 @@ +/* + * 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; + Map delegate; + + private MapDelegate(ConvertingImpl converting, Map del) { + convertingImpl = converting; + delegate = del; + } + + static MapDelegate forBean(Object b, Class< ? > beanClass, + ConvertingImpl converting) { + return new MapDelegate<>(converting, + new DynamicBeanFacade(b, beanClass, converting)); + } + + static Map forMap(Map m, ConvertingImpl converting) { + return new MapDelegate<>(converting, + new DynamicMapFacade<>(m, converting)); + } + + static MapDelegate forDictionary(Dictionary d, + ConvertingImpl converting) { + return new MapDelegate<>(converting, + new DynamicDictionaryFacade<>(d, converting)); + } + + static MapDelegate forDTO(Object obj, Class< ? > dtoClass, + ConvertingImpl converting) { + return new MapDelegate<>(converting, + new DynamicDTOFacade(obj, dtoClass, converting)); + } + + static MapDelegate forInterface(Object obj, Class< ? > intf, + ConvertingImpl converting) { + return new MapDelegate<>(converting, + 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); + } + + private Object getConvertedValue(Object val) { + return convertingImpl.convertMapValue(val); + } + + private Object findConvertedKey(Set< ? > keySet, Object key) { + for (Object k : keySet) { + if (key.equals(k)) + return k; + } + + for (Object k : keySet) { + Object c = convertingImpl.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..e6c8d1f0f20 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/SetDelegate.java @@ -0,0 +1,207 @@ +/* + * 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; + + static Set forCollection(Collection collection, + ConvertingImpl converting) { + if (collection instanceof Set) { + return new SetDelegate((Set) collection, converting); + } + return new SetDelegate(new CollectionSetDelegate<>(collection), + converting); + } + + SetDelegate(Set collection, ConvertingImpl converting) { + delegate = collection; + convertingImpl = converting; + } + + // 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); + } + + @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..7cb76d04896 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/Util.java @@ -0,0 +1,403 @@ +/* + * 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<>(); + for (Method md : beanClass.getDeclaredMethods()) { + 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 getSingleElementAnnotationKey(Class< ? > intf, Object obj) { + Class< ? > ann = getAnnotationType(intf, obj); + if (ann == null) + return null; + + boolean valueFound = false; + for (Method md : ann.getDeclaredMethods()) { + 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 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 { + Field prefixField = cls.getDeclaredField("PREFIX_"); + if (prefixField.getType().equals(String.class)) { + if ((prefixField.getModifiers() & (Modifier.PUBLIC + | Modifier.FINAL | Modifier.STATIC)) > 0) { + 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..3a4f06c0b66 --- /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") +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..2475990bb91 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/ConverterMapTest.java @@ -0,0 +1,606 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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.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 org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +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")); + } + + @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")); + } + + 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 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; + } + + 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..903e65d24bf --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/ConverterTest.java @@ -0,0 +1,1276 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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.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 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 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 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 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 @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()); + } + + 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/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/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..41b01123ae6 --- /dev/null +++ b/coordinator/pom.xml @@ -0,0 +1,103 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 3 + ../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 + + + 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..78442f422ce --- /dev/null +++ b/dependencymanager/.gitignore @@ -0,0 +1,3 @@ +/.gradle/ +/reports/ +/generated/ 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..4465945748c --- /dev/null +++ b/dependencymanager/build.gradle @@ -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. + */ + +/* + * Master Gradle build script + * + * Depends on bndPlugin property set by settings.gradle. + * and bnd_* values from gradle.properties. + */ + +import aQute.bnd.build.Workspace +import aQute.bnd.osgi.Constants + +/* Add bnd gradle plugin as a script dependency */ +buildscript { + dependencies { + classpath bndPlugin + } +} + +/* Initialize the bnd workspace */ +Workspace.setDriver(Constants.BNDDRIVER_GRADLE) +Workspace.addGestalt(Constants.GESTALT_BATCH, null) +ext.bndWorkspace = new Workspace(rootDir, bnd_cnf) +if (bndWorkspace == null) { + throw new GradleException("Unable to load workspace ${rootDir}/${bnd_cnf}") +} + +ext.cnf = rootProject.project(bnd_cnf) + +/* Configure the subprojects */ +subprojects { + def bndProject = bndWorkspace.getProject(name) + if (bndProject != null) { + plugins.apply 'biz.aQute.bnd' + } +} 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..0b7164229db --- /dev/null +++ b/dependencymanager/cnf/.project @@ -0,0 +1,17 @@ + + + cnf + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/dependencymanager/cnf/build.bnd b/dependencymanager/cnf/build.bnd new file mode 100644 index 00000000000..3ca494bc3dc --- /dev/null +++ b/dependencymanager/cnf/build.bnd @@ -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. +# + +######################## +## BND BUILD SETTINGS ## +######################## + + +## Global defaults are loaded from the bnd library (as shown below), place your +## specific settings here. Additional settings are inherited from ext/*.bnd and +## they will be overridden by anything you specify in this file. + +## General Options +#project.dependson: ${p-dependson;:} +#project.bootclasspath: ${p-bootclasspath;:} +#project.buildpath: ${p-buildpath;:} +#project.sourcepath: ${p-sourcepath;:} +#project.allsourcepath: ${p-allsourcepath;:} +#project.output: ${p-output} +#project.testpath: ${p-testpath;:} + +#-verbose: false +#project: ${basedir} +#src: src +#bin: bin +#testsrc: test +#testbin: bin_test +#test-reports: test-reports +#target-dir: generated +#target: ${project}/${target-dir} +#build: ${workspace}/cnf +#p: ${basename;${project}} +#project.name: ${p} +#plugin-dir: ${build}/plugins + +## Java Compiler Options +#java: java +#javac: javac +javac.source: 1.8 +javac.target: 1.8 +#javac.profile: +#javac.debug: on + +## Bnd Options +-sources: true +#-sourcepath: ${project}/src + + +## Properties from ext/*.bnd can be referenced in order to extend them. For +## example, to add one additional plugin to the list defined in +## ext/repositories.bnd: +# -plugin: ${ext.repositories.-plugin}, org.example.MyPlugin + + +## To enable baselining, uncomment the following lines: +-baseline: * + +## If you use git, you might want to uncomment the following lines: +# Git-Descriptor: ${system-allow-fail;git describe --dirty --always} +# Git-SHA: ${system-allow-fail;git rev-list -1 HEAD} +# -diffignore: Git-Descriptor,Git-SHA + +## +# Removes some headers in order to reduce binary diff between same bundles that are not changed between subsequent releases. +# see FELIX-4915 +# +-removeheaders: Bnd-LastModified,Tool,Created-By,Include-Resource + +# Make baselining issues a warning instead of error when using Eclipse +fixuptype: ${if;${is;${gestalt;batch};batch};error;warning} + +-fixupmessages: \ + "Baseline ...";is:=${fixuptype},\ + "The bundle version ...";is:=${fixuptype},\ + "The baseline version ...";is:=${fixuptype} + +# Generates poms in artifacts. +-pom: groupid = org.apache.felix diff --git a/dependencymanager/cnf/buildrepo/README.txt b/dependencymanager/cnf/buildrepo/README.txt new file mode 100644 index 00000000000..8bb02f154b9 --- /dev/null +++ b/dependencymanager/cnf/buildrepo/README.txt @@ -0,0 +1,6 @@ +WARNING +======= + +This directory contains JAR file dependencies that are intended ONLY FOR BUILT-TIME usage. +None are intended to be deployed as bundles into a running OSGi Framework, and indeed they may cause +unexpected errors if they are used at runtime. diff --git a/dependencymanager/cnf/buildrepo/biz.aQute.junit/biz.aQute.junit-latest.jar b/dependencymanager/cnf/buildrepo/biz.aQute.junit/biz.aQute.junit-latest.jar new file mode 100644 index 00000000000..bda8fe4ea0c Binary files /dev/null and b/dependencymanager/cnf/buildrepo/biz.aQute.junit/biz.aQute.junit-latest.jar differ diff --git a/dependencymanager/cnf/buildrepo/biz.aQute.launcher/biz.aQute.launcher-latest.jar b/dependencymanager/cnf/buildrepo/biz.aQute.launcher/biz.aQute.launcher-latest.jar new file mode 100644 index 00000000000..0e9c4a36d47 Binary files /dev/null and b/dependencymanager/cnf/buildrepo/biz.aQute.launcher/biz.aQute.launcher-latest.jar differ diff --git a/dependencymanager/cnf/buildrepo/junit.osgi/junit.osgi-3.8.2.jar b/dependencymanager/cnf/buildrepo/junit.osgi/junit.osgi-3.8.2.jar new file mode 100644 index 00000000000..1126b9526ff Binary files /dev/null and b/dependencymanager/cnf/buildrepo/junit.osgi/junit.osgi-3.8.2.jar differ diff --git a/dependencymanager/cnf/buildrepo/osgi.core/osgi.core-4.2.0.jar b/dependencymanager/cnf/buildrepo/osgi.core/osgi.core-4.2.0.jar new file mode 100644 index 00000000000..9ed943f01e1 Binary files /dev/null and b/dependencymanager/cnf/buildrepo/osgi.core/osgi.core-4.2.0.jar differ diff --git a/dependencymanager/cnf/ext/junit.bnd b/dependencymanager/cnf/ext/junit.bnd new file mode 100644 index 00000000000..f902dcfaf04 --- /dev/null +++ b/dependencymanager/cnf/ext/junit.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. +# +junit:\ + junit;version=latest,\ + hamcrest-core;version=latest + +test-reports: test-results diff --git a/dependencymanager/cnf/ext/libraries.bnd b/dependencymanager/cnf/ext/libraries.bnd new file mode 100644 index 00000000000..5222bdb18df --- /dev/null +++ b/dependencymanager/cnf/ext/libraries.bnd @@ -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. +# + +## +# 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.6,\ + org.apache.felix.gogo.jline;version=1.0.5,\ + org.jline;version=3.3.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=2.3.0,\ + org.apache.felix.http.servlet-api;version=1.0.0,\ + org.apache.felix.http.jetty;version="[2.3.0,2.3.0]",\ + org.apache.felix.webconsole;version=4.2.2 + +## +# bndlib +bndlib=biz.aQute.bndlib;version=3.0.0 + + + diff --git a/dependencymanager/cnf/ext/pluginpaths.bnd b/dependencymanager/cnf/ext/pluginpaths.bnd new file mode 100644 index 00000000000..228ed1607d8 --- /dev/null +++ b/dependencymanager/cnf/ext/pluginpaths.bnd @@ -0,0 +1,16 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 newline at end of file diff --git a/dependencymanager/cnf/ext/repositories.bnd b/dependencymanager/cnf/ext/repositories.bnd new file mode 100644 index 00000000000..67b0ebd4433 --- /dev/null +++ b/dependencymanager/cnf/ext/repositories.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. +# +-plugin: \ + aQute.bnd.deployer.repository.LocalIndexedRepo;\ + name=Release; \ + local=${workspace}/cnf/releaserepo; \ + pretty=true; \ + , \ + aQute.bnd.deployer.repository.LocalIndexedRepo; \ + name=Local; \ + local=${workspace}/cnf/localrepo; \ + pretty=true; \ + , \ + aQute.bnd.deployer.repository.FixedIndexedRepo; \ + name=Bndtools Hub; \ + locations=https://raw.githubusercontent.com/bndtools/bundle-hub/master/index.xml.gz; \ + , \ + aQute.lib.deployer.FileRepo; \ + name=Non OSGi; \ + location=${workspace}/cnf/nonosgi-repo; \ + latest=false; + +-releaserepo: Release +-baselinerepo: Release diff --git a/dependencymanager/cnf/localrepo/index.xml b/dependencymanager/cnf/localrepo/index.xml new file mode 100644 index 00000000000..046ae227430 --- /dev/null +++ b/dependencymanager/cnf/localrepo/index.xml @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..7e220702d3b --- /dev/null +++ b/dependencymanager/cnf/localrepo/index.xml.sha @@ -0,0 +1 @@ +de12209a6bcd5df83b3010910cef6ee640a01eecce08469f8dff901da50e0bac \ No newline at end of file diff --git a/dependencymanager/cnf/localrepo/org.apache.felix.gogo.command/org.apache.felix.gogo.command-1.0.2.jar b/dependencymanager/cnf/localrepo/org.apache.felix.gogo.command/org.apache.felix.gogo.command-1.0.2.jar new file mode 100644 index 00000000000..67d224f3b6e Binary files /dev/null and b/dependencymanager/cnf/localrepo/org.apache.felix.gogo.command/org.apache.felix.gogo.command-1.0.2.jar differ diff --git a/dependencymanager/cnf/localrepo/org.apache.felix.gogo.jline/org.apache.felix.gogo.jline-1.0.6.jar b/dependencymanager/cnf/localrepo/org.apache.felix.gogo.jline/org.apache.felix.gogo.jline-1.0.6.jar new file mode 100644 index 00000000000..1febda6a2c9 Binary files /dev/null and b/dependencymanager/cnf/localrepo/org.apache.felix.gogo.jline/org.apache.felix.gogo.jline-1.0.6.jar differ diff --git a/dependencymanager/cnf/localrepo/org.apache.felix.gogo.runtime/org.apache.felix.gogo.runtime-1.0.6.jar b/dependencymanager/cnf/localrepo/org.apache.felix.gogo.runtime/org.apache.felix.gogo.runtime-1.0.6.jar new file mode 100644 index 00000000000..1e3c8f09021 Binary files /dev/null and b/dependencymanager/cnf/localrepo/org.apache.felix.gogo.runtime/org.apache.felix.gogo.runtime-1.0.6.jar differ diff --git a/dependencymanager/cnf/localrepo/org.apache.felix.http.api/org.apache.felix.http.api-2.3.0.jar b/dependencymanager/cnf/localrepo/org.apache.felix.http.api/org.apache.felix.http.api-2.3.0.jar new file mode 100644 index 00000000000..d9df5d70435 Binary files /dev/null and b/dependencymanager/cnf/localrepo/org.apache.felix.http.api/org.apache.felix.http.api-2.3.0.jar differ diff --git a/dependencymanager/cnf/localrepo/org.apache.felix.http.servlet-api/org.apache.felix.http.servlet-api-1.0.0.jar b/dependencymanager/cnf/localrepo/org.apache.felix.http.servlet-api/org.apache.felix.http.servlet-api-1.0.0.jar new file mode 100644 index 00000000000..49841e12cb1 Binary files /dev/null and b/dependencymanager/cnf/localrepo/org.apache.felix.http.servlet-api/org.apache.felix.http.servlet-api-1.0.0.jar differ diff --git a/dependencymanager/cnf/localrepo/org.jline/org.jline-3.3.0.jar b/dependencymanager/cnf/localrepo/org.jline/org.jline-3.3.0.jar new file mode 100644 index 00000000000..8a27d2fe7bf Binary files /dev/null and b/dependencymanager/cnf/localrepo/org.jline/org.jline-3.3.0.jar differ diff --git a/dependencymanager/cnf/nonosgi-repo/README.txt b/dependencymanager/cnf/nonosgi-repo/README.txt new file mode 100644 index 00000000000..5e4d3482811 --- /dev/null +++ b/dependencymanager/cnf/nonosgi-repo/README.txt @@ -0,0 +1,6 @@ +WARNING +======= + +This directory contains JAR file dependencies that are intended ONLY FOR BUILD-TIME usage. +None are intended to be deployed as bundles into a running OSGi Framework, and indeed they may cause +unexpected errors if they are used at runtime. diff --git a/dependencymanager/cnf/nonosgi-repo/hamcrest-core/hamcrest-core-1.3.0.jar b/dependencymanager/cnf/nonosgi-repo/hamcrest-core/hamcrest-core-1.3.0.jar new file mode 100644 index 00000000000..9d5fe16e3dd Binary files /dev/null and b/dependencymanager/cnf/nonosgi-repo/hamcrest-core/hamcrest-core-1.3.0.jar differ diff --git a/dependencymanager/cnf/nonosgi-repo/junit/junit-4.11.0.jar b/dependencymanager/cnf/nonosgi-repo/junit/junit-4.11.0.jar new file mode 100644 index 00000000000..aaf74448492 Binary files /dev/null and b/dependencymanager/cnf/nonosgi-repo/junit/junit-4.11.0.jar differ diff --git a/dependencymanager/cnf/plugins/biz.aQute.repository/biz.aQute.repository.jar b/dependencymanager/cnf/plugins/biz.aQute.repository/biz.aQute.repository.jar new file mode 100644 index 00000000000..fc7c813a104 Binary files /dev/null and b/dependencymanager/cnf/plugins/biz.aQute.repository/biz.aQute.repository.jar differ diff --git a/dependencymanager/cnf/releaserepo/index.xml b/dependencymanager/cnf/releaserepo/index.xml new file mode 100644 index 00000000000..0f31a269691 --- /dev/null +++ b/dependencymanager/cnf/releaserepo/index.xml @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..07848e4ab2d --- /dev/null +++ b/dependencymanager/cnf/releaserepo/index.xml.sha @@ -0,0 +1 @@ +2127e7aad0d5972cd255ba1e56e34abef6e272bebfd292429185c6090c88f842 \ No newline at end of file diff --git a/dependencymanager/cnf/releaserepo/org.apache.felix.dependencymanager.annotation/org.apache.felix.dependencymanager.annotation-4.2.1.jar b/dependencymanager/cnf/releaserepo/org.apache.felix.dependencymanager.annotation/org.apache.felix.dependencymanager.annotation-4.2.1.jar new file mode 100644 index 00000000000..abd7e432319 Binary files /dev/null and b/dependencymanager/cnf/releaserepo/org.apache.felix.dependencymanager.annotation/org.apache.felix.dependencymanager.annotation-4.2.1.jar differ diff --git a/dependencymanager/cnf/releaserepo/org.apache.felix.dependencymanager.lambda/org.apache.felix.dependencymanager.lambda-1.1.1.jar b/dependencymanager/cnf/releaserepo/org.apache.felix.dependencymanager.lambda/org.apache.felix.dependencymanager.lambda-1.1.1.jar new file mode 100644 index 00000000000..132d6c91973 Binary files /dev/null and b/dependencymanager/cnf/releaserepo/org.apache.felix.dependencymanager.lambda/org.apache.felix.dependencymanager.lambda-1.1.1.jar differ diff --git a/dependencymanager/cnf/releaserepo/org.apache.felix.dependencymanager.runtime/org.apache.felix.dependencymanager.runtime-4.0.5.jar b/dependencymanager/cnf/releaserepo/org.apache.felix.dependencymanager.runtime/org.apache.felix.dependencymanager.runtime-4.0.5.jar new file mode 100644 index 00000000000..e1f091283bf Binary files /dev/null and b/dependencymanager/cnf/releaserepo/org.apache.felix.dependencymanager.runtime/org.apache.felix.dependencymanager.runtime-4.0.5.jar differ diff --git a/dependencymanager/cnf/releaserepo/org.apache.felix.dependencymanager.shell/org.apache.felix.dependencymanager.shell-4.0.6.jar b/dependencymanager/cnf/releaserepo/org.apache.felix.dependencymanager.shell/org.apache.felix.dependencymanager.shell-4.0.6.jar new file mode 100644 index 00000000000..8090afc633a Binary files /dev/null and b/dependencymanager/cnf/releaserepo/org.apache.felix.dependencymanager.shell/org.apache.felix.dependencymanager.shell-4.0.6.jar differ diff --git a/dependencymanager/cnf/releaserepo/org.apache.felix.dependencymanager/org.apache.felix.dependencymanager-4.4.1.jar b/dependencymanager/cnf/releaserepo/org.apache.felix.dependencymanager/org.apache.felix.dependencymanager-4.4.1.jar new file mode 100644 index 00000000000..290c66e719a Binary files /dev/null and b/dependencymanager/cnf/releaserepo/org.apache.felix.dependencymanager/org.apache.felix.dependencymanager-4.4.1.jar differ diff --git a/ipojo/src/main/java/org/apache/felix/ipojo/DummyActivator.java b/dependencymanager/cnf/src/.gitignore similarity index 100% rename from ipojo/src/main/java/org/apache/felix/ipojo/DummyActivator.java rename to dependencymanager/cnf/src/.gitignore 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..b97353339ee --- /dev/null +++ b/dependencymanager/gradle.properties @@ -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. + */ + +# http(s) proxy settings + +#systemProp.http.proxyHost= +#systemProp.http.proxyPort= +#systemProp.http.proxyUser= +#systemProp.http.proxyPassword= +#systemProp.http.nonProxyHosts=*.nonproxyrepos.com|localhost +#systemProp.https.proxyHost= +#systemProp.https.proxyPort= +#systemProp.https.proxyUser= +#systemProp.https.proxyPassword= +#systemProp.https.nonProxyHosts=*.nonproxyrepos.com|localhost + +# cnf project name +bnd_cnf=cnf + +# bnd_plugin is the dependency declaration for the bnd gradle plugin +bnd_plugin=biz.aQute.bnd:biz.aQute.bnd.gradle:3.3.0 + +# bnd_build can be set to the name of a "master" project whose dependencies will seed the set of projects to build. +bnd_build= + +# Default gradle task to build +bnd_defaultTask=build + +# This should be false. It only needs to be true in rare cases. +bnd_preCompileRefresh=false diff --git a/dependencymanager/gradle/wrapper/gradle-wrapper.jar b/dependencymanager/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000..3d0dee6e8ed Binary files /dev/null and b/dependencymanager/gradle/wrapper/gradle-wrapper.jar differ diff --git a/dependencymanager/gradle/wrapper/gradle-wrapper.properties b/dependencymanager/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..1b2a875f8db --- /dev/null +++ b/dependencymanager/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Jan 15 16:04:00 CET 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-bin.zip diff --git a/dependencymanager/gradlew b/dependencymanager/gradlew new file mode 100755 index 00000000000..91a7e269e19 --- /dev/null +++ b/dependencymanager/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# 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 +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# 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\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +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" ] ; 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"` + + # 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 + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/.classpath b/dependencymanager/org.apache.felix.dependencymanager.annotation/.classpath new file mode 100644 index 00000000000..57c70f3fbf6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/.classpath @@ -0,0 +1,8 @@ + + + + + + + + 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..b43b1e281d1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/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. +# +Bundle-Version: 4.2.1 +-buildpath: \ + biz.aQute.bndlib;version=2.4,\ + osgi.core;version=6.0,\ + osgi.cmpn;version=6.0 +Private-Package: \ + org.apache.felix.dm.annotation.plugin.bnd +Export-Package: \ + org.apache.felix.dm.annotation.api +Include-Resource: META-INF/=resources/ +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/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/dependencymanager/org.apache.felix.dependencymanager.annotation/resources/changelog.txt b/dependencymanager/org.apache.felix.dependencymanager.annotation/resources/changelog.txt new file mode 100644 index 00000000000..087a264ad19 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/resources/changelog.txt @@ -0,0 +1,188 @@ +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 + +** Improvement + * [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-5365] - Generate warning on service published and consumed on same interface but different bundles + * [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 + +** 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 + +** New Feature + * [FELIX-4689] - Create a more fluent syntax for the dependency manager builder + +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: +====================================================================== + +** 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 + +** 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 + +** New Feature + * [FELIX-4426] - Allow DM to manage collections of services + * [FELIX-4807] - New thread model for Dependency Manager + +** 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-4818] - New release process for Dependency Manager diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/.gitignore new file mode 100644 index 00000000000..e69de29bb2d 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..cb0fe5daa53 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/AdapterService.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.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. 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 a service property (param=value), and will also include eventual + * service properties found from the AdapteeService: + *

      + *
      + * 
      + * @AdapterService(adapteeService = AdapteeService.class)
      + * @Property(name="param", value="value")
      + * class AdapterImpl implements AdapterService {
      + *     // The service we are adapting (injected by reflection)
      + *     protected AdapteeService adaptee;
      + *   
      + *     public void doWork() {
      + *        adaptee.mehod1();
      + *        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) + */ + 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 in 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 ""; + + /** + * 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. + * @return the service propagation flag + */ + boolean propagate() default true; +} 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..70f835cf2c6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/AspectService.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.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. It will also inherit all dependencies, + * and 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))
      + * @Property(name="param", value="value")
      + * class AspectService implements InterceptedService {
      + *     // The service we are intercepting (injected by reflection)
      + *     protected 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. + */ + 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 in 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 ""; +} 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..c559e408004 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/BundleAdapterService.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.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 {
      + *     Bundle bundle; // Injected by reflection
      + *     
      + *     void play() {
      + *         URL mpegFile = bundle.getEntry(bundle.getHeaders().get("Video-Path"));
      + *         // play the video provided by the bundle ...
      + *     }
      + *     
      + *     void stop() {}
      + * }
      + * 
      + *
      + * + * @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 + */ + Property[] properties() default {}; + + /** + * The filter used to match a given 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. + * @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 ""; +} 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..273f9bc5b00 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/BundleDependency.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.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 service have changed. + * @return the change callback + */ + String changed() default ""; + + /** + * Returns the callback method to invoke when the service 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 filter dependency + * @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. + * @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..8f9a4436d70 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Component.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.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 factoryName attribute is set, the component is not started automatically + * during bundle startup, and a org.apache.felix.dm.runtime.api.ComponentFactory + * object is registered into the OSGi registry on behalf of the component. This ComponentFactory + * can then be used by another component in order to instantiate multiple instances of the component + * (DM ComponentFactory are really similar to DS ComponentFactory). + * + *

      Usage Examples

      + * + * Here is a sample showing a X 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 X implements Z {
      + *     @ConfigurationDependency(pid="MyPid")
      + *     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 Z service.
      + *     }
      + *   
      + *     public void doService() {
      + *         // ...
      + *     }   
      + * }
      + * 
      + *
      + * + * Here is a sample showing how a Y component may dynamically instantiate several X component instances, + * using the {@link #factoryName()} attribute: + *
      + * + *
      + *  /**
      + *    * All component instances will be created/updated/removed by the "Y" component
      + *    */
      + *  @Component(factoryName="MyComponentFactory", factoryConfigure="configure")
      + *  class X implements Z {                 
      + *      void configure(Dictionary conf) {
      + *          // Configure or reconfigure our component. The conf is provided by the factory,
      + *          // and all public properties (which don't start with a dot) are propagated with the
      + *          // service properties specified in the properties annotation attribute.
      + *      }
      + * 
      + *      @ServiceDependency
      + *      void bindOtherService(OtherService other) {
      + *          // store this require dependency
      + *      }
      + *      
      + *      @Start
      + *      void start() {
      + *          // Our component is starting and is about to be registered in the OSGi registry as a Z service.
      + *      } 
      + *      
      + *      public void doService() {
      + *          // ...
      + *      }   
      + *  }
      + * 
      + *  import import org.apache.felix.dm.runtime.api.ComponentFactory;
      + *
      + *  /**
      + *    * This class will instantiate some X component instances
      + *    */
      + *  @Component 
      + *  class Y {
      + *      @ServiceDependency(filter="(" + Component.FACTORY_NAME + "=MyComponentFactory)")
      + *      ComponentFactory _XFactory;
      + *    
      + *      @Start
      + *      void start() {
      + *          // Instantiate a X component instance
      + *          Dictionary instance1Conf = new Hashtable() {{ put("foo", "bar1"); }};
      + *          ComponentInstance instance1 = _XFactory.newInstance(instance1Conf);
      + *      
      + *          // Instantiate another X component instance
      + *          Dictionary instance2Conf = new Hashtable() {{ put("foo2", "bar2"); }};
      + *          ComponentInstance instance2 = _XFactory.newInstance(instance2Conf);
      + *      
      + *          // Update the first X component instance
      + *          instance1Conf = new Hashtable() {{ put("foo", "bar1 modified"); }};
      + *          instance1.update(instance1Conf);
      + *          
      + *          // Instantiate a third X instance, by explicitly providing the implementation object
      + *          Dictionary instance3Conf = new Hashtable() {{ put(Component.FACTORY_INSTANCE, new X()); }};
      + *          ComponentInstance instance3 = _XFactory.newInstance(instance3Conf);
      + *      
      + *          // Destroy x1/x2/x3 components
      + *          instance1.dispose();
      + *          instance2.dispose();
      + *          instance3.dispose();
      + *      }
      + *  }
      + * 
      + * + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface Component +{ + /** + * Service property name used to match a given Component Factory. + * @see #factoryName() for more information about factory sets. + */ + final static String FACTORY_NAME = "dm.factory.name"; + + /** + * Key used when providing an implementation when using a Component Factory . + * @see #factoryName() + */ + final static String FACTORY_INSTANCE = "dm.factory.instance"; + + /** + * Sets list of provided interfaces. By default, the directly implemented interfaces are provided. + * @return the provided interfaces + */ + Class[] provides() default {}; + + /** + * 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. + */ + Property[] properties() default {}; + + /** + * Returns the name of the Factory Set used to dynamically instantiate this component. + * When you set this attribute, a java.util.Set<java.lang.Dictionary> OSGi Service will + * be provided with a dm.factory.name service property matching your specified factorySet attribute. + * This Set will be provided once the component bundle is started, even if required dependencies are not available, and the + * Set will be unregistered from the OSGi registry once the component bundle is stopped or being updated.

      + * So, basically, another component may then be injected with this set in order to dynamically instantiate some component instances: + *

        + *
      • Each time a new Dictionary is added into the Set, then a new instance of the annotated component will be instantiated.
      • + *
      • Each time an existing Dictionary is re-added into the Set, then the corresponding component instance will be updated.
      • + *
      • Each time an existing Dictionary is removed from the Set, then the corresponding component instance will be destroyed.
      • + *
      + * + *

      The dictionary registered in the Set will be provided to the created component instance using a callback method that you can + * optionally specify in the {@link Component#factoryConfigure()} attribute. Each public properties from that dictionary + * (which don't start with a dot) will be propagated along with the annotated component service properties. + * + *

      Optionally, the dictionary registered into the factory set may provide an implementation instance for the component to be created, + * using the {@value #FACTORY_INSTANCE} key. + * + * @deprecated use {@link #factoryName()} instead of a factorySet. + * @return the factory set name + */ + String factorySet() default ""; + + /** + * Returns the name of the ComponentFactory used to dynamically instantiate this component. + * When you set this attribute, a org.apache.felix.dm.runtime.api.ComponentFactory OSGi Service will + * be provided with a dm.factory.name service property matching your specified factoryName attribute. + * + * The ComponentFactory will be provided once the component bundle is started, even if required dependencies are not available, and the + * ComponentFactory will be unregistered from the OSGi registry once the component bundle is stopped or being updated.

      + * So, another component may then be injected with this ComponentFactory in order to dynamically instantiate some component instances: + * + *

      The dictionary passed to the ComponentFactory.newInstance method will be provided to the created component instance using a callback + * method that you can optionally specify in the {@link Component#factoryConfigure()} attribute. Each public properties from that dictionary + * (which don't start with a dot) will be propagated along with the annotated component service properties. + * + *

      Optionally, the dictionary registered into the factory set may provide an implementation instance for the component to be created, + * using a "dm.runtime.factory.instance" key. + * @return the factory name + */ + String factoryName() default ""; + + /** + * Sets "configure" callback method name to be called with the factory configuration. This attribute only makes sense if the + * {@link #factoryName()} attribute is used. If specified, then this attribute references a callback method, which is called + * for providing the configuration supplied by the factory that instantiated this component. The current component service properties will be + * also updated with all public properties (which don't start with a dot). + * @return the factory configure callback name + */ + String factoryConfigure() default ""; + + /** + * Sets the static method used to create the components implementation instance. + * @return the factory method used to instantiate the component + */ + String factoryMethod() 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..3ee24354343 --- /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}, {@link AdapterService}, {@link FactoryConfigurationAdapterService} or + * {@link ResourceAdapterService} annotation. + * + *

      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 {
      + *     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..8c0eb088b26 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ConfigurationDependency.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.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. + * + * The annotation can be applied on a callback method which accepts the following parameters: + * + *

        + *
      • callback(Dictionary) + *
      • callback(Component, Dictionary) + *
      • callback(Configuration interface) // type safe configuration + *
      • callback(Component, Configuration interface) // type safe configuration + *
      + * + *

      Usage Examples

      + * + *

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

      First, we define the configuration metadata, using standard bndtools metatatype annotations + * (see http://www.aqute.biz/Bnd/MetaType): + * + *

      + *
      + * package sample;
      + * import aQute.bnd.annotation.metatype.Meta.AD;
      + * import aQute.bnd.annotation.metatype.Meta.OCD;
      + *
      + * @OCD(description = "Declare here the Printer Configuration.")
      + * public interface PrinterConfiguration {
      + *     @AD(description = "Enter the printer ip address")
      + *     String getAddress();
      + *
      + *     @AD(description = "Enter the printer address port number.")
      + *     default int getPort() { return 8080; }
      + * }
      + * 
      + *
      + * + * Next, we define our Printer service which depends on the PrinterConfiguration: + * + *
      + *
      + * package sample;
      + * import aQute.bnd.annotation.metatype.*;
      + *
      + * @Component
      + * public class Printer {
      + *     @ConfigurationDependency // Will use the fqdn of the  PrinterConfiguration interface as the pid.
      + *     void updated(PrinterConfiguration cnf) {
      + *         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). + * @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. + * You can use this method when you use an interface annotated with standard bndtols metatype annotations. + * (see http://www.aqute.biz/Bnd/MetaType). + * @return the pid class + * @deprecated just define an updated callback which accepts as argument a configuration type. + */ + 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. + * @return true if configuration must be published along with the service, false if not. + */ + boolean propagate() default false; + + /** + * 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 ""; + + /** + * 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 + */ + boolean required() default true; + + /** + * 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. + * @deprecated use standard bndtools metatype annotations instead (see http://www.aqute.biz/Bnd/MetaType) + */ + String heading() default ""; + + /** + * 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. + * @deprecated use standard bndtools metatype annotations instead (see http://www.aqute.biz/Bnd/MetaType) + */ + String description() default ""; + + /** + * The list of properties types used to expose properties in web console. + * @return The list of properties types used to expose properties in web console. + * @deprecated use standard bndtools metatype annotations instead (see http://www.aqute.biz/Bnd/MetaType) + */ + PropertyMetaData[] metadata() 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/FactoryConfigurationAdapterService.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/FactoryConfigurationAdapterService.java new file mode 100644 index 00000000000..a95689e0edd --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/FactoryConfigurationAdapterService.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.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 class that acts as a Factory Configuration Adapter Service. For each new Config Admin + * factory configuration matching the specified factoryPid, an instance of this service will be created. + * 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. + * + *

      If you specify a configuration type, then the fqdn of the configuration interface is used as the factory pid, + * else you can specify the factory pid explicitly using the factoryPid attribute. + * If no configuration type is used and no factoryPid attribute is specified, then the factory pid will be set to the fqdn of + * the class on which this annotation is applied. + * + *

      (see javadoc from {@link ConfigurationDependency} for more informations about configuration types). + * + *

      Usage Examples

      + * Here, a "Dictionary" service instance is created for each existing "sample.DictionaryConfiguration" factory pids. + * + * First, we declare our factory configuration metadata using standard bndtools metatatype annotations + * (see http://www.aqute.biz/Bnd/MetaType): + * + *
      + *
      + * package sample;
      + * import java.util.List;
      + * import aQute.bnd.annotation.metatype.Meta.AD;
      + * import aQute.bnd.annotation.metatype.Meta.OCD;
      + *
      + * @OCD(factory = true, description = "Declare here some Dictionary instances.")
      + * public interface DictionaryConfiguration {
      + *   @AD(description = "Describes the dictionary language.", deflt = "en")
      + *   String lang();
      + *
      + *   @AD(description = "Declare here the list of words supported by this dictionary.")
      + *   List<String> words();
      + * }
      + * 
      + *
      + * + * And here is the factory pid adapter service, which is instantiated for each instance of the "sample.DictionaryConfiguration" factory pid: + * + *
      + *
      + * import java.util.List;
      + * import aQute.bnd.annotation.metatype.Configurable;
      + *
      + * @FactoryConfigurationAdapterService(configType=DictionaryConfiguration.class)  
      + * public class DictionaryImpl implements DictionaryService {
      + *     protected void updated(DictionaryConfiguration config) {
      + *         m_lang = config.lang();
      + *         m_words.clear();
      + *         for (String word : conf.words()) {
      + *             m_words.add(word);
      + *         }
      + *     }
      + *     ...
      + * }
      + * 
      + *
      + * + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface FactoryConfigurationAdapterService +{ + /** + * The interface(s) to use when registering adapters. By default, directly implemented + * interfaces will be registered in the OSGi registry. + * @return the registered service interfaces + */ + Class[] provides() default {}; + + /** + * Adapter Service properties. Notice that public factory configuration is also registered in service properties, + * (only if propagate is true). Public factory configuration properties are those which don't starts with a dot ("."). + * @return the adapter service properties + */ + Property[] properties() default {}; + + /** + * Returns the type safe configuration class which will be injected in the updated callback. + * By default, the factory pid is assumed to match the fqdn of the configuration type. + * see javadoc from {@link ConfigurationDependency} for more informations about configuration types. + * @return the configuration type to pass in the "updated" callback argument. + * @see ConfigurationDependency + */ + Class configType() default Object.class; + + /** + * Returns the factory pid whose configurations will instantiate the annotated service class. (By default, the pid is the + * service class name). + * @return the factory pid + */ + String factoryPid() default ""; + + /** + * Returns the factory pid from a class name. The full class name will be used as the configuration PID. + * You can use this method when you use an interface annoted with standard bndtols metatype annotations. + * (see http://www.aqute.biz/Bnd/MetaType). + * @return the factory pid class + * @deprecated use {@link #configType()} and accept a configuration type parameter from your updated callback. The pid + * is then assumed to match the fqdn of the configuration type. + */ + Class factoryPidClass() default Object.class; + + /** + * The Update method to invoke (defaulting to "updated"), when a factory configuration is created or updated + * @return the updated callback + */ + String updated() default "updated"; + + /** + * Returns true if the configuration properties must be published along with the service. + * Any additional service properties specified directly are merged with these. + * @return true if configuration must be published along with the service, false if not. + */ + boolean propagate() default false; + + /** + * 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. + * @deprecated use standard bndtools metatype annotations instead (see http://www.aqute.biz/Bnd/MetaType) + */ + String heading() default ""; + + /** + * 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. + * @deprecated use standard bndtools metatype annotations instead (see http://www.aqute.biz/Bnd/MetaType) + */ + String description() default ""; + + /** + * The list of properties types used to expose properties in web console. + * @return The list of properties types used to expose properties in web console. + * @deprecated use standard bndtools metatype annotations instead (see http://www.aqute.biz/Bnd/MetaType) + */ + PropertyMetaData[] metadata() default {}; + + /** + * Sets the static method used to create the adapter instance. + * @return the factory method + */ + String factoryMethod() default ""; +} 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..4eb71d0aeca --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Init.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; + +/** + * 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 dynamic + * dependencies are injected, then the start method (annotated with @Start) is then invoked. + * + *

      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.
      + *     @ServiceDependency
      + *     LogService log;
      + *     
      + *     // 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;
      + *  
      + *     // Injected after init (dependency filter is defined dynamically from our init method).
      + *     @ServiceDependency(name="storage")
      + *     Storage storage;
      + * 
      + *     // Dynamically configure the dependency declared with a "storage" name.
      + *     @Init
      + *     Map<String, String> init() {
      + *        log.log(LogService.LOG_WARNING, "init: storage type=" + storageType + ", storageRequired=" + storageRequired);
      + *        Map<String, String> props = new HashMap<>();
      + *        props.put("storage.required", Boolean.toString(_xmlConfiguration.isStorageRequired()))
      + *        props.put("storage.filter", "(type=" + _xmlConfiguration.getStorageType() + ")");
      + *        return props;       
      + *     }
      + *     
      + *     // 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..9c55b88b2c4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Property.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.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. It is used for example when + * declaring {@link Component#properties()} attribute.

      + * + * 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/PropertyMetaData.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/PropertyMetaData.java new file mode 100644 index 00000000000..5a701d85960 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/PropertyMetaData.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.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * This annotation describes the data types of a configuration Property. + * It can be used by other annotations which require meta type support. + * For now, the following annotations are using PropertyMetaData: + *
        + *
      • {@link ConfigurationDependency}: This dependency allows to define a + * dependency over a Configuration Admin configuration dictionaries, whose + * metadata can be described using PropertyMetaData annotation. + *
      • {@link FactoryConfigurationAdapterService}: This service adapter allows + * to dynamically create Services on behalf of Factory Configuration Admin + * configuration dictionaries, whose metadata can be described using this PropertyMetaData annotation. + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.ANNOTATION_TYPE) +public @interface PropertyMetaData +{ + /** + * The label used to display the property. Example: "Log Level". + * @return The label used to display the property + */ + String heading(); + + /** + * The key of a ConfigurationAdmin property. Example: "printer.logLevel" + * @return The Configuration Admin property name + */ + String id(); + + /** + * Return 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
      • + *
      + * @return the property type + */ + Class type() default String.class; + + /** + * Return default value(s) 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. + * @return the default values + */ + String[] defaults() default {}; + + /** + * 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 The localized description of the definition. + */ + 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 lower than 0 -x = max occurrences, store in Vector
      • + *
      • x greater than 0 x = max occurrences, store in array []
      • + *
      • x equals Integer.MAX_VALUE no limit, but use array []
      • + *
      • x equals 0 1 occurrence required
      • + *
      + * @return the property cardinality + */ + int cardinality() default 0; + + /** + * Tells if this property is required or not. + * @return true if the property is required, false if not + */ + boolean required() default true; + + /** + * Return a list of valid option labels for this property. The purpose of this method is to allow menus with localized labels. + * It is associated with the {@link #optionValues()} attribute. The labels returned here are ordered in the same way as the + * {@link #optionValues()} attribute values. + * @return the list of valid option labels for this property. + */ + String[] optionLabels() default {}; + + /** + * Return a list of option values that this property can take. This list must be in the same sequence as the {@link #optionLabels()} + * attribute. + * @return the option values + */ + String[] optionValues() default {}; +} 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/ResourceAdapterService.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ResourceAdapterService.java new file mode 100644 index 00000000000..92d4114224b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ResourceAdapterService.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.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a class as a Resource adapter service. Resource adapters are things that + * adapt a resource instead of a service, and provide an adapter service on top of this resource. + * Resources are an abstraction that is introduced by the dependency manager, represented as a URL. + * They can be implemented to serve resources embedded in bundles, somewhere on a file system or in + * an http content repository server, or database.

      + * The adapter will be applied to any resource that matches the specified filter condition, which can + * match some part of the resource URL (with "path", "protocol", "port", or "host" filters). + * 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 with any extra service properties + * you supply here. Moreover, the following service properties will be propagated from the resource URL: + * + *

      • "host": this property exposes the host part of the resource URL + *
      • "path": the resource URL path + *
      • "protocol": the resource URL protocol + *
      • "port": the resource URL port + *
      + * + *

      Usage Examples

      + * Here, the "VideoPlayer" service provides a video service on top of any movie resources, with service + * properties "host"/"port"/"protocol"/"path" extracted from the resource URL: + *
      + *
      + * 
      + * @ResourceAdapterService(filter = "(&(path=/videos/*.mkv)(host=localhost))", propagate = true)
      + * public class VideoPlayerImpl implements VideoPlayer {
      + *     // Injected by reflection
      + *     URL resource;
      + *     
      + *     void play() {} // play video referenced by this.resource     
      + *     void stop() {} // stop playing the video
      + *     void transcode() {} // ...
      + * }
      + * 
      + *
      + * + * And here is an example of a VideoProvider, which provides some videos using a web URL. + * Notice that Resource providers need to depend on the DependencyManager API: + * + *
      + *
      + * import java.net.MalformedURLException;
      + * import java.net.URL;
      + * import java.util.HashMap;
      + * import java.util.Map;
      + * 
      + * import org.apache.felix.dm.ResourceHandler;
      + * import org.apache.felix.dm.ResourceUtil;
      + * 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.osgi.framework.BundleContext;
      + * import org.osgi.framework.Filter;
      + * import org.osgi.framework.InvalidSyntaxException;
      + * 
      + * @Component
      + * public class VideoProvider
      + * {
      + *     // Injected by reflection
      + *     private volatile BundleContext context;
      + *     // List of known resource handlers
      + *     private Map<ResourceHandler, Filter> m_handlers = new HashMap<ResourceHandler, Filter>();
      + *     // List of known video resources
      + *     private URL[] m_videos;
      + * 
      + *     @Init
      + *     void init() throws MalformedURLException
      + *     {
      + *        m_videos = new URL[] {
      + *                new URL("http://localhost:8080/videos/video1.mkv"),
      + *                new URL("http://localhost:8080/videos/video2.mkv"),
      + *         };
      + *     }
      + * 
      + *     // Track resource handlers
      + *     @ServiceDependency(required = false)
      + *     public void add(Map<String, String> serviceProperties, ResourceHandler handler) throws InvalidSyntaxException
      + *     {
      + *         String filterString = serviceProperties.get("filter");
      + *         filterString = (filterString != null) ? filterString : "(path=*)";
      + *         Filter filter = context.createFilter(filterString);
      + *         synchronized (this)
      + *         {
      + *             m_handlers.put(handler, filter);
      + *         }
      + *         for (URL video : m_videos)
      + *         {
      + *             if (filter.match(ResourceUtil.createProperties(video)))
      + *             {
      + *                 handler.added(video);
      + *             }
      + *         }
      + *     }
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface ResourceAdapterService +{ + /** + * The interface(s) to use when registering adapters + * @return the provided interfaces + */ + Class[] provides() default {}; + + /** + * Additional properties to use with the adapter service registration + * @return the properties + */ + Property[] properties() default {}; + + /** + * The filter condition to use with the resource. + * @return the filter + */ + String filter(); + + /** + * true if properties from the resource should be propagated to the service properties. + * @return the propagate flag + */ + boolean propagate() default false; + + /** + * The callback method to be invoked when the Resource has changed. + * @return the changed callback + */ + String changed() default ""; + + /** + * Sets the static method used to create the AdapterService implementation instance. + * @return the factory method + */ + String factoryMethod() default ""; +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ResourceDependency.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ResourceDependency.java new file mode 100644 index 00000000000..b62863dc826 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ResourceDependency.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.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a method of field as a Resource Dependency. A resource dependency allows you to + * depend on a resource. Resources are an abstraction that is introduced by the dependency manager, represented as a URL. + * They can be implemented to serve resources embedded in bundles, somewhere on a file system or in + * an http content repository server, or database.

      A resource is a URL and you can use a filter condition based on + * protocol, host, port, and path. + * + *

      Usage Examples

      + * Here, the "VideoPlayer" component plays any provided MKV video resources + *
      + *
      + * 
      + * @Component
      + * public class VideoPlayer {
      + *     @ResourceDependency(required=false, filter="(path=/videos/*.mkv)")
      + *     void playResource(URL video) { ... }
      + * }
      + * 
      + *
      + * + * And here is an example of a VideoProvider, which provides some videos using a web URL. + * Notice that Resource providers need to depend on the DependencyManager API: + * + *
      + *
      + * import java.net.MalformedURLException;
      + * import java.net.URL;
      + * import java.util.HashMap;
      + * import java.util.Map;
      + * 
      + * import org.apache.felix.dm.ResourceHandler;
      + * import org.apache.felix.dm.ResourceUtil;
      + * 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.osgi.framework.BundleContext;
      + * import org.osgi.framework.Filter;
      + * import org.osgi.framework.InvalidSyntaxException;
      + * 
      + * @Component
      + * public class VideoProvider
      + * {
      + *     // Injected by reflection
      + *     private volatile BundleContext context;
      + *     // List of known resource handlers
      + *     private Map<ResourceHandler, Filter> m_handlers = new HashMap<ResourceHandler, Filter>();
      + *     // List of known video resources
      + *     private URL[] m_videos;
      + * 
      + *     @Init
      + *     void init() throws MalformedURLException
      + *     {
      + *        m_videos = new URL[] {
      + *                new URL("http://localhost:8080/videos/video1.mkv"),
      + *                new URL("http://localhost:8080/videos/video2.mkv"),
      + *         };
      + *     }
      + * 
      + *     // Track resource handlers
      + *     @ServiceDependency(required = false)
      + *     public void add(Map<String, String> serviceProperties, ResourceHandler handler) throws InvalidSyntaxException
      + *     {
      + *         String filterString = serviceProperties.get("filter");
      + *         filterString = (filterString != null) ? filterString : "(path=*)";
      + *         Filter filter = context.createFilter(filterString);
      + *         synchronized (this)
      + *         {
      + *             m_handlers.put(handler, filter);
      + *         }
      + *         for (URL video : m_videos)
      + *         {
      + *             if (filter.match(ResourceUtil.createProperties(video)))
      + *             {
      + *                 handler.added(video);
      + *             }
      + *         }
      + *     }
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.METHOD, ElementType.FIELD}) +public @interface ResourceDependency +{ + /** + * Returns 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 ""; + + /** + * Returns the callback method to be invoked when the service properties have changed. + * @return the change callback + */ + String changed() default ""; + + /** + * Returns the callback method to invoke when the service is lost. + * @return the remove callback + */ + String removed() default ""; + + /** + * Returns whether the Service dependency is required or not. + * @return the required flag + */ + boolean required() default true; + + /** + * Returns the Service dependency OSGi filter. + * @return the filter + */ + String filter() default ""; + + /** + * Specifies if the resource URL properties must be propagated. If set to true, then the URL properties + * ("protocol"/"host"/"port"/"path") will be propagated to the service properties of the component which + * is using this dependency. + * @return the propagate 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 + */ + String name() default ""; +} 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..c42c3b7a364 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ServiceDependency.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.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 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 MyComponent component is injected with a dependency over a "MyDependency" service + * + *
      + * @Component
      + * class MyComponent {
      + *     @ServiceDependency(timeout=15000)
      + *     MyDependency dependency;
      + * 
      + * + * @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 ""; + + /** + * The class for the default implementation, if the dependency is not available. + * @return the default implementation class + */ + Class defaultImpl() default Object.class; + + /** + * Whether the Service dependency is required or not. + * @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. + * @return true if dependency service properties must be published along with the service, false if not. + */ + boolean propagate() default false; + + /** + * 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 dereference(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). + */ + boolean dereference() default true; +} 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..d96c0b8c06d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/packageinfo @@ -0,0 +1 @@ +version 1.2.0 \ No newline at end of file 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..4033a75455d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/AnnotationCollector.java @@ -0,0 +1,1543 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * 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.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +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.FactoryConfigurationAdapterService; +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.Registered; +import org.apache.felix.dm.annotation.api.RepeatableProperty; +import org.apache.felix.dm.annotation.api.ResourceAdapterService; +import org.apache.felix.dm.annotation.api.ResourceDependency; +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.osgi.framework.Bundle; + +import aQute.bnd.osgi.Annotation; +import aQute.bnd.osgi.ClassDataCollector; +import aQute.bnd.osgi.Clazz; +import aQute.bnd.osgi.Descriptors.TypeRef; +import aQute.bnd.osgi.Verifier; + +/** + * 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_RESOURCE_DEPENDENCY = ResourceDependency.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_RESOURCE_ADAPTER_SERVICE = ResourceAdapterService.class.getName(); + private final static String A_FACTORYCONFIG_ADAPTER_SERVICE = FactoryConfigurationAdapterService.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 Logger m_logger; + private String[] m_interfaces; + private boolean m_isField; + private String m_field; + private String m_method; + private String m_descriptor; + private final Set m_dependencyNames = new HashSet(); + private final List m_writers = new ArrayList(); + private MetaType m_metaType; + 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_componentField; + private String m_registeredMethod; + private String m_unregisteredMethod; + private TypeRef m_superClass; + private boolean m_baseClass = true; + + /** + * 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); + + /** + * Makes a new Collector for parsing a given class. + * @param reporter the object used to report logs. + */ + public AnnotationCollector(Logger reporter, MetaType metaType) + { + m_logger = reporter; + m_metaType = metaType; + } + + /** + * 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", method.getName(), method.getDescriptor()); + m_isField = false; + m_method = method.getName(); + m_descriptor = method.getDescriptor().toString(); + } + + /** + * 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_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_RESOURCE_ADAPTER_SERVICE)) + { + parseResourceAdapterService(annotation); + } + else if (name.equals(A_FACTORYCONFIG_ADAPTER_SERVICE)) + { + parseFactoryConfigurationAdapterService(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_RESOURCE_DEPENDENCY)) + { + parseRersourceDependencyAnnotation(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; + } + } + + /** + * 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 a single Property declared on the component type (in this case, there is no @Repeatable annotation). + + if (m_singleProperty != null) { + parseProperty(m_singleProperty, componentWriter); + } + } + + /** + * 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) + { + 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(); + } + + // factorySet attribute (deprecated, replaced by factoryName) + String factorySetName = writer.putString(annotation, EntryParam.factorySet, null); + if (factorySetName != null) + { + // When a component defines a factorySet, it means that a java.util.Set will + // be provided into the OSGi registry, in order to let anoter component add + // some component instance configurations into it. + // So, we have to indicate that the Set is provided as a service, in the Export-Serviec + // header. + m_exportService.add("java.util.Set"); + } + + // factoryName attribute + String factoryName = writer.putString(annotation, EntryParam.factoryName, null); + if (factoryName != null) + { + // When a component defines a factoryName, it means that a ComponentFactory will + // be provided into the OSGi registry, in order to let another component create some component instances. + // So, we have to indicate that the ComponentFactory is provided as a service, in the Export-Serviec + // header. + m_exportService.add("org.apache.felix.dependencymanager.runtime.api.ComponentFactory"); + } + + // factoryConfigure attribute + writer.putString(annotation, EntryParam.factoryConfigure, null); + + // 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); + } + } + + /** + * 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); + 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); + } + 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(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, null); + } + + /** + * 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); + + // The pid is either: + // + // - the fqdn of the configuration proxy type, if the callback accepts an interface (not a Dictionary). + // - or the fqdn of the class specified by the pidFromClass attribute + // - or the value of the pid attribute + // - or by default the fdqn of the class where the annotation is found + + String pidFromClass = parseClassAttrValue(annotation.get(EntryParam.pidClass.toString())); + String pid = pidFromClass != null ? pidFromClass : get(annotation, EntryParam.pid.toString(), null); + + // Check if annotation is applied on "updated(ConfigProxyType)" + String confProxyType = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS5, 1, false); + if (confProxyType != null) + { + if (! Dictionary.class.getName().equals(confProxyType)) + { + // It's a conf proxy type. + writer.put(EntryParam.configType, confProxyType); + } + else + { + confProxyType = null; + } + + } + else + { + // Check if annotation is applied on "updated(Component, ConfigProxyType)" + confProxyType = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS2, 2, false); + if (! Dictionary.class.getName().equals(confProxyType)) + { + // It's a conf proxy type. + writer.put(EntryParam.configType, confProxyType); + } + else + { + confProxyType = null; + } + } + + if (pid == null) + { + if (confProxyType != null) + { + pid = confProxyType; + } + else + { + pid = m_componentClassName; + } + } + + writer.put(EntryParam.pid, pid); + + // the method on which the annotation is applied + writer.put(EntryParam.updated, m_method); + + // propagate attribute + writer.putString(annotation, EntryParam.propagate, null); + + // required flag (true by default) + writer.putString(annotation, EntryParam.required, Boolean.TRUE.toString()); + + // Property Meta Types + parseMetaTypes(annotation, pid, false); + + // 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 BundleAdapterService annotation. + * @param annotation + */ + private void parseResourceAdapterService(Annotation annotation) + { + EntryWriter writer = new EntryWriter(EntryType.ResourceAdapterService); + m_writers.add(writer); + + // Generate Adapter Implementation + writer.put(EntryParam.impl, m_componentClassName); + + // Parse resource filter + String filter = annotation.get(EntryParam.filter.toString()); + if (filter != null) + { + Verifier.verifyFilter(filter, 0); + writer.put(EntryParam.filter, filter); + } + + // 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 propagate attribute + writer.putString(annotation, EntryParam.propagate, Boolean.FALSE.toString()); + + // Parse changed attribute + writer.putString(annotation, EntryParam.changed, null); + } + + /** + * Parses a Factory Configuration Adapter annotation. + * @param annotation + */ + private void parseFactoryConfigurationAdapterService(Annotation annotation) + { + EntryWriter writer = new EntryWriter(EntryType.FactoryConfigurationAdapterService); + m_writers.add(writer); + + // Generate Adapter Implementation + writer.put(EntryParam.impl, m_componentClassName); + + // factory pid attribute (can be specified using the factoryPid attribute, or using the factoryPidClass attribute) + String factoryPidClass = parseClassAttrValue(annotation.get(EntryParam.factoryPidClass.toString())); + + // Test if a type safe configuration type is provided. + String configType = parseClassAttrValue(annotation.get(EntryParam.configType.toString())); + + if (configType != null) { + writer.put(EntryParam.configType, configType); + } + + String factoryPid = null; + + factoryPid = get(annotation, EntryParam.factoryPid.toString(), factoryPidClass); + if (factoryPid == null) { + factoryPid = configType != null ? configType : m_componentClassName; + } + + writer.put(EntryParam.factoryPid, factoryPid); + + // 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 optional meta types for configuration description. + parseMetaTypes(annotation, factoryPid, true); + + // Parse factoryMethod attribute + writer.putString(annotation, EntryParam.factoryMethod, null); + } + + 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); + } + + private void parseRersourceDependencyAnnotation(Annotation annotation) + { + checkDependencyAlreadyDeclaredInChild(annotation, ! m_isField ? m_method : m_field, ! m_isField); + + EntryWriter writer = new EntryWriter(EntryType.ResourceDependency); + m_writers.add(writer); + + String filter = annotation.get(EntryParam.filter.toString()); + if (filter != null) + { + Verifier.verifyFilter(filter, 0); + writer.put(EntryParam.filter, filter); + } + + if (m_isField) + { + writer.put(EntryParam.autoConfig, m_field); + } + + writer.putString(annotation, EntryParam.added, (!m_isField) ? m_method : null); + writer.putString(annotation, EntryParam.changed, null); + writer.putString(annotation, EntryParam.removed, null); + writer.putString(annotation, EntryParam.required, null); + writer.putString(annotation, EntryParam.propagate, null); + writer.putString(annotation, EntryParam.factoryMethod, 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; + } + } + + /** + * Parse optional meta types annotation attributes + * @param annotation + */ + private void parseMetaTypes(Annotation annotation, String pid, boolean factory) + { + if (annotation.get("metadata") != null) + { + String propertiesHeading = annotation.get("heading"); + String propertiesDesc = annotation.get("description"); + + MetaType.OCD ocd = new MetaType.OCD(pid, propertiesHeading, propertiesDesc); + for (Object p: (Object[]) annotation.get("metadata")) + { + Annotation property = (Annotation) p; + String heading = property.get("heading"); + String id = property.get("id"); + String type = parseClassAttrValue(property.get("type")); + Object[] defaults = (Object[]) property.get("defaults"); + String description = property.get("description"); + Integer cardinality = property.get("cardinality"); + Boolean required = property.get("required"); + + MetaType.AD ad = new MetaType.AD(id, type, defaults, heading, description, + cardinality, required); + + Object[] optionLabels = property.get("optionLabels"); + Object[] optionValues = property.get("optionValues"); + + if (optionLabels == null + && optionValues != null + || + optionLabels != null + && optionValues == null + || + (optionLabels != null && optionValues != null && optionLabels.length != optionValues.length)) + { + throw new IllegalArgumentException("invalid option labels/values specified for property " + + id + + " in PropertyMetadata annotation from class " + m_currentClassName); + } + + if (optionValues != null) + { + for (int i = 0; i < optionValues.length; i++) + { + ad.add(new MetaType.Option(optionValues[i].toString(), optionLabels[i].toString())); + } + } + + ocd.add(ad); + } + + m_metaType.add(ocd); + MetaType.Designate designate = new MetaType.Designate(pid, factory); + m_metaType.add(designate); + m_logger.info("Parsed MetaType Properties from class " + m_componentClassName); + } + } + + /** + * 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); + } + } + } + + /** + * 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 + { + 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; + } +} \ No newline at end of file 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..6fe7140eb33 --- /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("error: " + t.toString(), 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..d69c4123e64 --- /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, Object ... args) { + m_reporter.error(content, args); + logErr(String.format(content, args), null); + } + + public void error(String content, Throwable err, Object ... args) { + m_reporter.error(content, args); + logErr(String.format(content, args), err); + } + + /** + * 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..802c0db25b4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/DescriptorGenerator.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.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(), "*"); + + // Create the object which will collect Config Admin MetaTypes. + MetaType metaType = new MetaType(); + + for (Clazz c : expanded) + { + AnnotationCollector reader = new AnnotationCollector(m_logger, metaType); + 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()); + } + } + + // If some Meta Types have been parsed, then creates the corresponding resource file. + if (metaType.getSize() > 0) + { + m_metaTypeResource = createMetaTypeResource(metaType); + } + 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); + } + + /** + * Creates a bnd resource that contains the generated metatype descriptor. + * @param metaType the Object that has collected all meta type informations. + * @return the meta type resource + * @throws IOException on any errors + */ + private Resource createMetaTypeResource(MetaType metaType) throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, "UTF-8")); + metaType.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..426feea5581 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/EntryParam.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.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 + factoryPid, + factoryPidClass, + propagate, + updated, + timeout, + adapteeService, + adapteeFilter, + stateMask, + ranking, + factorySet, + factoryName, + factoryConfigure, + factoryMethod, + field, + name, + starter, + stopper, + bundleContextField, + dependencyManagerField, + componentField, + registrationField, + registered, + unregistered, + dereference +} 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..60b2b636471 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/EntryWriter.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.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 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/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/MetaType.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/MetaType.java new file mode 100644 index 00000000000..b934306311e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/MetaType.java @@ -0,0 +1,326 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * 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.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Helper class used to generate an XML representation of a MetaType data structure. + * + * @author Felix Project Team + */ +public class MetaType +{ + /** + * The list of Object Class Definitions used to group the attributes of a given + * set of properties. + */ + private List m_ocdList = new ArrayList(); + + /** + * The list of Designate elements. + */ + private List m_designateList = new ArrayList(); + + /** + * The default localization directory. + */ + private final static String LOCALIZATION = "OSGI-INF/metatype/metatype"; + + /** + * Adds an Object Class Definition into this meta type. + * @param ocd the Object Class Definition. + */ + public void add(OCD ocd) + { + m_ocdList.add(ocd); + } + + /** + * Adds a Designate element, which maps a PID to an OCD. + * @param designate the Designate element. + */ + public void add(Designate designate) + { + m_designateList.add(designate); + } + + /** + * Returns the number of OCD contained in this meta type. + * @return the number of OCD contained in this meta type. + */ + public int getSize() + { + return m_ocdList.size(); + } + + /** + * Generates an XML representation of this metatype. + * @param pw a PrintWriter where the XML is written + */ + public void writeTo(PrintWriter pw) + { + pw.println(""); + pw.println(""); + for (OCD ocd : m_ocdList) + { + ocd.writeTo(pw); + } + for (Designate designate : m_designateList) + { + designate.writeTo(pw); + } + pw.println(""); + } + + private static void writeAttribute(String name, Object value, PrintWriter pw) + { + if (value != null) + { + pw.print(" " + name + "=" + "\"" + value.toString() + "\""); + } + } + + /** + * An Object Class Definition, which contains a set of Attributes properies. + */ + public static class OCD + { + String m_id; + String m_name; + String m_description; + List m_attributes = new ArrayList(); + + OCD(String pid, String name, String desc) + { + this.m_id = pid; + this.m_name = name; + this.m_description = desc; + } + + public void add(AD ad) + { + m_attributes.add(ad); + } + + public void writeTo(PrintWriter pw) + { + pw.print(" "); + } + else + { + pw.println(">"); + for (AD ad : m_attributes) + { + ad.writeTo(pw); + } + pw.println(" "); + } + } + } + + /** + * An Attribute Definition, which describes a given Properties + */ + @SuppressWarnings("serial") + public static class AD + { + String m_id; + String m_type; + String m_defaults; + String m_name; + String m_description; + Integer m_cardinality; + Boolean m_required; + List