You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by jb...@apache.org on 2020/05/03 04:56:38 UTC
[karaf] branch master updated: interceptor support on top on OSGi
services
This is an automated email from the ASF dual-hosted git repository.
jbonofre pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/karaf.git
The following commit(s) were added to refs/heads/master by this push:
new a9a60e7 interceptor support on top on OSGi services
new d983b75 Merge pull request #993 from rmannibucau/interceptor-poc
a9a60e7 is described below
commit a9a60e789d34939e3b707752267ac3b915ed41a1
Author: Romain Manni-Bucau <rm...@apache.org>
AuthorDate: Mon Nov 11 19:27:23 2019 +0100
interceptor support on top on OSGi services
---
.../src/main/asciidoc/user-guide/interceptor.adoc | 112 ++++
pom.xml | 1 +
scr/state/pom.xml | 2 +-
services/interceptor/api/pom.xml | 58 ++
.../service/interceptor/api/AroundInvoke.java | 40 ++
.../interceptor/api/EnableInterceptors.java | 34 ++
.../karaf/service/interceptor/api/Interceptor.java | 33 +
.../interceptor/api/InterceptorBinding.java | 28 +
.../service/interceptor/api/InvocationContext.java | 34 ++
.../interceptor/api/RequireInterceptorImpl.java | 35 ++
services/interceptor/impl/pom.xml | 231 +++++++
.../impl/activator/InterceptorActivator.java | 96 +++
.../impl/runtime/ComponentProperties.java | 22 +
.../interceptor/impl/runtime/Exceptions.java | 36 ++
.../impl/runtime/PropertiesManager.java | 38 ++
.../interceptor/impl/runtime/ProxiesManager.java | 133 +++++
.../runtime/hook/InterceptedInstancesHooks.java | 63 ++
.../impl/runtime/hook/InterceptorInstance.java | 89 +++
.../invoker/InterceptorInvocationContext.java | 106 ++++
.../impl/runtime/invoker/package-info.java | 19 +
.../impl/runtime/proxy/AsmProxyFactory.java | 663 +++++++++++++++++++++
.../impl/runtime/proxy/ProxyFactory.java | 222 +++++++
.../registry/InterceptedServiceRegistry.java | 142 +++++
.../impl/runtime/registry/InterceptorRegistry.java | 97 +++
.../karaf/service/interceptor/impl/E2ETest.java | 118 ++++
.../impl/runtime/proxy/AsmProxyFactoryTest.java | 72 +++
.../interceptor/impl/test/InterceptedService.java | 34 ++
.../service/interceptor/impl/test/Suffix.java | 32 +
.../impl/test/SuffixingInterceptor.java | 32 +
.../karaf/service/interceptor/impl/test/Wrap.java | 31 +
.../interceptor/impl/test/WrappingInterceptor.java | 32 +
...ce.interceptor.impl.test.InterceptedService.xml | 25 +
....interceptor.impl.test.SuffixingInterceptor.xml | 25 +
...e.interceptor.impl.test.WrappingInterceptor.xml | 25 +
services/interceptor/pom.xml | 40 ++
services/pom.xml | 1 +
36 files changed, 2800 insertions(+), 1 deletion(-)
diff --git a/manual/src/main/asciidoc/user-guide/interceptor.adoc b/manual/src/main/asciidoc/user-guide/interceptor.adoc
new file mode 100644
index 0000000..8291de7
--- /dev/null
+++ b/manual/src/main/asciidoc/user-guide/interceptor.adoc
@@ -0,0 +1,112 @@
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+= Interceptor
+
+Interceptor module is inspired from JavaEE/JakartaEE interceptor API but adapted to OSGi services.
+
+It enables to proxy any service and execute code around the service methods.
+
+== Dependencies
+
+[source,xml]
+----
+<dependency>
+ <groupId>org.apache.karaf.services</groupId>
+ <artifactId>org.apache.karaf.services.interceptor.api</artifactId>
+</dependency>
+----
+
+== Defining an interceptor
+
+An interceptor is simply an OSGi service marked with `@Interceptor` and having an interceptor binding which is nothing more than an annotation marked with `@InterceptorBinding`.
+
+Here is a binding:
+
+[source,java]
+----
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+@InterceptorBinding
+public @interface Suffix {
+}
+----
+
+And here is an associated interceptor:
+
+[source,java]
+----
+@Suffix
+@Interceptor
+@Component(service = SuffixingInterceptor.class)
+public class SuffixingInterceptor {
+ // ...
+}
+----
+
+TIP: the examples are using SCR but there is no requirement to do so, you can do it with `context.registerService()` as well.
+
+For the interceptor to do something, you must define an `@AroundInvoke` method which will intercept method calls in intercepted services.
+It must takes a single parameter of type `InvocationContext`:
+
+[source,java]
+----
+@AroundInvoke
+public Object around(final InvocationContext context) throws Exception {
+ return context.proceed() + "(suffixed)";
+}
+----
+
+== Using interceptors
+
+Assuming you have an interceptor library (it is commong for transversal concerns like security, auditing, tracing, metrics, etc...), you can enable the interceptor usages with these few steps:
+
+. Ensure you register your service as an OSGi service,
+. Mark the service with `@EnableInterceptors`,
+. Mark the class or method with the interceptor bindings you want
+
+TIP: if you put a binding on a class is it available for all methods and is called after method level interceptors.
+
+As an example speaks better than 1000 words, here is a service using our previous suffixing interceptor:
+
+[source,java]
+----
+@EnableInterceptors
+@Component(service = InterceptedService.class)
+public class InterceptedService {
+ @Suffix
+ public String doStuff(final String value) {
+ return "'" + value + "'";
+ }
+}
+----
+
+You can notice that it is equivalent to the following example which just moved the interceptor at class level:
+
+
+[source,java]
+----
+@Suffix
+@EnableInterceptors
+@Component(service = InterceptedService.class)
+public class InterceptedService {
+ public String doStuff(final String value) {
+ return "'" + value + "'";
+ }
+}
+----
+
+== Proxying implementation
+
+If possible, the proxying will use `java.lang.reflect.Proxy` but if there is a class to proxy and not only interfaces, `asm` must be available for the proxy to suceed to be created.
diff --git a/pom.xml b/pom.xml
index 0e52375..076b155 100644
--- a/pom.xml
+++ b/pom.xml
@@ -321,6 +321,7 @@
<websocket.version>1.1</websocket.version>
<winsw.version>2.3.0</winsw.version>
+ <osgi-component-annotations.version>1.4.0</osgi-component-annotations.version>
<surefire.argLine />
diff --git a/scr/state/pom.xml b/scr/state/pom.xml
index 265aef0..cf4348d 100644
--- a/scr/state/pom.xml
+++ b/scr/state/pom.xml
@@ -62,7 +62,7 @@
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.service.component.annotations</artifactId>
- <version>1.3.0</version>
+ <version>${osgi-component-annotations.version}</version>
</dependency>
<dependency>
<groupId>org.apache.felix</groupId>
diff --git a/services/interceptor/api/pom.xml b/services/interceptor/api/pom.xml
new file mode 100644
index 0000000..e8e6dda
--- /dev/null
+++ b/services/interceptor/api/pom.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.karaf.services</groupId>
+ <artifactId>org.apache.karaf.services.interceptor</artifactId>
+ <version>4.3.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>org.apache.karaf.services.interceptor.api</artifactId>
+ <packaging>bundle</packaging>
+ <name>Apache Karaf :: Services :: Interceptor :: API</name>
+ <description>Interceptor API.</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.component.annotations</artifactId>
+ <version>${osgi-component-annotations.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <configuration>
+ <instructions>
+ <Export-Package>org.apache.karaf.service.interceptor.api</Export-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/AroundInvoke.java b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/AroundInvoke.java
new file mode 100644
index 0000000..a2e7306
--- /dev/null
+++ b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/AroundInvoke.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.karaf.service.interceptor.api;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Marks an interceptor public method as the intercepting one.
+ *
+ * It must be:
+ * <ul>
+ * <li>public</li>
+ * <li>have a single {@link InvocationContext} parameter</li>
+ * </ul>
+ *
+ * It can optionally throw {@link Exception}.
+ */
+@Target(METHOD)
+@Retention(RUNTIME) // no support of methods in SCR so we use plain reflection
+@RequireInterceptorImpl
+public @interface AroundInvoke {
+}
diff --git a/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/EnableInterceptors.java b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/EnableInterceptors.java
new file mode 100644
index 0000000..c07b09e
--- /dev/null
+++ b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/EnableInterceptors.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.karaf.service.interceptor.api;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.osgi.service.component.annotations.ComponentPropertyType;
+
+// note: would be better to make it a stereotype but bnd does not support it yet so let's make it a wrapper
+@Target(TYPE)
+@Retention(CLASS)
+@ComponentPropertyType
+@RequireInterceptorImpl
+public @interface EnableInterceptors {
+ String PREFIX_ = "apache.karaf."; // we don't want just "interceptor" as property key
+}
diff --git a/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/Interceptor.java b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/Interceptor.java
new file mode 100644
index 0000000..c3c5993
--- /dev/null
+++ b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/Interceptor.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.karaf.service.interceptor.api;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.osgi.service.component.annotations.ComponentPropertyType;
+
+@Target(TYPE)
+@Retention(CLASS)
+@ComponentPropertyType
+@RequireInterceptorImpl
+public @interface Interceptor {
+ String PREFIX_ = "apache.karaf."; // we don't want just "interceptor" as property key
+}
diff --git a/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/InterceptorBinding.java b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/InterceptorBinding.java
new file mode 100644
index 0000000..d084451
--- /dev/null
+++ b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/InterceptorBinding.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.karaf.service.interceptor.api;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(RUNTIME)
+@Target(ANNOTATION_TYPE)
+public @interface InterceptorBinding {
+}
diff --git a/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/InvocationContext.java b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/InvocationContext.java
new file mode 100644
index 0000000..a157726
--- /dev/null
+++ b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/InvocationContext.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.karaf.service.interceptor.api;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+
+public interface InvocationContext {
+ Object getTarget();
+
+ Method getMethod();
+
+ Object[] getParameters();
+
+ void setParameters(Object[] var1);
+
+ Map<String, Object> getContextData();
+
+ Object proceed() throws Exception;
+}
\ No newline at end of file
diff --git a/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/RequireInterceptorImpl.java b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/RequireInterceptorImpl.java
new file mode 100644
index 0000000..25b4ef1
--- /dev/null
+++ b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/RequireInterceptorImpl.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.karaf.service.interceptor.api;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(CLASS)
+@Target(TYPE)
+/* when on r7
+@Requirement(
+ namespace = "osgi.implementation",
+ name = "org.apache.karaf.service.interceptor.impl",
+ version = "${project.version}" // java-template plugin?
+)
+*/
+@interface RequireInterceptorImpl {
+}
\ No newline at end of file
diff --git a/services/interceptor/impl/pom.xml b/services/interceptor/impl/pom.xml
new file mode 100644
index 0000000..66915b9
--- /dev/null
+++ b/services/interceptor/impl/pom.xml
@@ -0,0 +1,231 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.karaf.services</groupId>
+ <artifactId>org.apache.karaf.services.interceptor</artifactId>
+ <version>4.3.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>org.apache.karaf.services.interceptor.impl</artifactId>
+ <packaging>bundle</packaging>
+ <name>Apache Karaf :: Services :: Interceptor :: Implementation</name>
+ <description>Interceptor implementation.</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>org.apache.karaf.services.interceptor.api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>osgi.core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ow2.asm</groupId>
+ <artifactId>asm</artifactId>
+ <version>${asm.version}</version>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.component.annotations</artifactId>
+ <version>${osgi-component-annotations.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ops4j.pax.exam</groupId>
+ <artifactId>pax-exam-junit4</artifactId>
+ <version>${pax.exam.version}</version>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.configadmin</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.ops4j.pax.exam</groupId>
+ <artifactId>pax-exam-container-karaf</artifactId>
+ <version>${pax.exam.version}</version>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.configadmin</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.karaf</groupId>
+ <artifactId>apache-karaf</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ <type>tar.gz</type>
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.geronimo.specs</groupId>
+ <artifactId>geronimo-atinject_1.0_spec</artifactId>
+ <version>1.1</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-jdk14</artifactId>
+ <version>${slf4j.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.karaf.specs</groupId>
+ <artifactId>org.apache.karaf.specs.locator</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <profiles>
+ <profile>
+ <id>java9-plus</id>
+ <activation>
+ <jdk>[9,)</jdk>
+ </activation>
+ <dependencies>
+ <dependency>
+ <groupId>jakarta.xml.bind</groupId>
+ <artifactId>jakarta.xml.bind-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish.jaxb</groupId>
+ <artifactId>jaxb-runtime</artifactId>
+ </dependency>
+ </dependencies>
+ </profile>
+ </profiles>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-dependency-plugin</artifactId>
+ <version>3.1.1</version>
+ <executions>
+ <execution>
+ <id>copy</id>
+ <phase>generate-test-resources</phase>
+ <goals>
+ <goal>copy</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <artifactItems>
+ <artifactItem>
+ <groupId>org.ow2.asm</groupId>
+ <artifactId>asm</artifactId>
+ <version>${asm.version}</version>
+ <type>jar</type>
+ <overWrite>true</overWrite>
+ <outputDirectory>${project.build.directory}/libs</outputDirectory>
+ <destFileName>asm.jar</destFileName>
+ </artifactItem>
+ <artifactItem>
+ <groupId>org.apache.karaf</groupId>
+ <artifactId>apache-karaf</artifactId>
+ <version>${project.version}</version>
+ <type>tar.gz</type>
+ <overWrite>true</overWrite>
+ <outputDirectory>${project.build.directory}/libs</outputDirectory>
+ <destFileName>karaf.tar.gz</destFileName>
+ </artifactItem>
+ </artifactItems>
+ <overWriteReleases>false</overWriteReleases>
+ <overWriteSnapshots>true</overWriteSnapshots>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <configuration>
+ <instructions>
+ <Bundle-Activator>org.apache.karaf.service.interceptor.impl.activator.InterceptorActivator</Bundle-Activator>
+ <Export-Package>org.apache.karaf.service.interceptor.impl.activator</Export-Package>
+ <Private-Package>
+ org.apache.karaf.service.interceptor.impl.runtime,
+ org.apache.karaf.service.interceptor.impl.runtime.hook,
+ org.apache.karaf.service.interceptor.impl.runtime.invoker,
+ org.apache.karaf.service.interceptor.impl.runtime.proxy,
+ org.apache.karaf.service.interceptor.impl.runtime.registry
+ </Private-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>default-test</id>
+ <goals>
+ <goal>test</goal>
+ </goals>
+ <configuration>
+ <excludes>
+ <exclude>**/E2E*</exclude>
+ </excludes>
+ </configuration>
+ </execution>
+ <execution>
+ <id>e2e</id>
+ <phase>package</phase>
+ <goals>
+ <goal>test</goal>
+ </goals>
+ <configuration>
+ <includes>
+ <include>**/E2E*</include>
+ </includes>
+ </configuration>
+ </execution>
+ </executions>
+ <configuration>
+ <trimStackTrace>false</trimStackTrace>
+ <systemPropertyVariables>
+ <java.util.logging.SimpleFormatter.format>%1$tF %1$tT [%4$s] [%2$-89s] %5$s%6$s%n</java.util.logging.SimpleFormatter.format>
+ <karaf.version>${project.version}</karaf.version>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/activator/InterceptorActivator.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/activator/InterceptorActivator.java
new file mode 100644
index 0000000..8fd01ba
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/activator/InterceptorActivator.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.karaf.service.interceptor.impl.activator;
+
+import static java.util.Optional.ofNullable;
+import static org.apache.karaf.service.interceptor.impl.runtime.ComponentProperties.INTERCEPTORS_PROPERTY;
+import static org.apache.karaf.service.interceptor.impl.runtime.ComponentProperties.INTERCEPTOR_PROPERTY;
+
+import java.util.Hashtable;
+import java.util.stream.Stream;
+
+import org.apache.karaf.service.interceptor.impl.runtime.PropertiesManager;
+import org.apache.karaf.service.interceptor.impl.runtime.ProxiesManager;
+import org.apache.karaf.service.interceptor.impl.runtime.hook.InterceptedInstancesHooks;
+import org.apache.karaf.service.interceptor.impl.runtime.proxy.ProxyFactory;
+import org.apache.karaf.service.interceptor.impl.runtime.registry.InterceptedServiceRegistry;
+import org.apache.karaf.service.interceptor.impl.runtime.registry.InterceptorRegistry;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.framework.hooks.service.EventListenerHook;
+import org.osgi.framework.hooks.service.FindHook;
+
+public class InterceptorActivator implements BundleActivator {
+ private InterceptorRegistry interceptorRegistry;
+ private InterceptedServiceRegistry interceptedServiceRegistry;
+ private ProxiesManager proxiesManager;
+
+ private ServiceRegistration<?> hooksRegistration;
+
+ @Override
+ public void start(final BundleContext context) throws InvalidSyntaxException {
+ final PropertiesManager propertiesManager = new PropertiesManager();
+ // todo: decouple these three services with a bus? here we use the activator to keep it simple
+ interceptedServiceRegistry = new InterceptedServiceRegistry(this::onServiceAddition, this::onServiceRemoval, propertiesManager);
+ interceptorRegistry = new InterceptorRegistry(this::onInterceptorAddition, this::onInterceptorRemoval, propertiesManager);
+ proxiesManager = new ProxiesManager(interceptorRegistry, interceptedServiceRegistry, new ProxyFactory(), propertiesManager);
+
+ // listen for interceptors and intercepted instances to be able to react on (un)registrations
+ context.addServiceListener(interceptedServiceRegistry, "(" + INTERCEPTORS_PROPERTY + "=true)");
+ context.addServiceListener(interceptorRegistry, "(" + INTERCEPTOR_PROPERTY + "=true)");
+
+ // register existing services/interceptors
+ ofNullable(context.getAllServiceReferences(null, "(" + INTERCEPTORS_PROPERTY + "=true)"))
+ .ifPresent(refs -> Stream.of(refs).forEach(ref -> interceptedServiceRegistry.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, ref))));
+ ofNullable(context.getAllServiceReferences(null, "(" + INTERCEPTOR_PROPERTY + "=true)"))
+ .ifPresent(refs -> Stream.of(refs).forEach(ref -> interceptorRegistry.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, ref))));
+
+ // ensure we filter out the proxied services to only return proxies
+ hooksRegistration = context.registerService(
+ new String[]{FindHook.class.getName(), EventListenerHook.class.getName()},
+ new InterceptedInstancesHooks(context.getBundle().getBundleId()),
+ new Hashtable<>());
+ }
+
+ @Override
+ public void stop(final BundleContext context) {
+ context.removeServiceListener(interceptorRegistry);
+ context.removeServiceListener(interceptedServiceRegistry);
+ hooksRegistration.unregister();
+ proxiesManager.stop();
+ }
+
+ private void onServiceAddition(final ServiceReference<?> ref) {
+ proxiesManager.onInterceptedInstanceAddition(ref);
+ }
+
+ private void onServiceRemoval(final ServiceReference<?> ref) {
+ proxiesManager.onInterceptedInstanceRemoval(ref);
+ }
+
+ private void onInterceptorAddition(final Class<?> aClass) {
+ proxiesManager.onInterceptorAddition(aClass);
+ }
+
+ private void onInterceptorRemoval(final Class<?> aClass) {
+ proxiesManager.onInterceptorRemoval(aClass);
+ }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/ComponentProperties.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/ComponentProperties.java
new file mode 100644
index 0000000..ff842be
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/ComponentProperties.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.apache.karaf.service.interceptor.impl.runtime;
+
+public interface ComponentProperties {
+ String INTERCEPTORS_PROPERTY = "apache.karaf.enable.interceptors";
+ String INTERCEPTOR_PROPERTY = "apache.karaf.interceptor";
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/Exceptions.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/Exceptions.java
new file mode 100644
index 0000000..37fa376
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/Exceptions.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime;
+
+import java.lang.reflect.InvocationTargetException;
+
+public final class Exceptions {
+ private Exceptions() {
+ // no-op
+ }
+
+ public static Object unwrap(final InvocationTargetException ite) throws Exception {
+ final Throwable targetException = ite.getTargetException();
+ if (Exception.class.isInstance(targetException)) {
+ throw Exception.class.cast(targetException);
+ }
+ if (Error.class.isInstance(targetException)) {
+ throw Error.class.cast(targetException);
+ }
+ throw ite; // quite unlikely
+ }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/PropertiesManager.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/PropertiesManager.java
new file mode 100644
index 0000000..f610cec
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/PropertiesManager.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.karaf.service.interceptor.impl.runtime;
+
+import java.util.Hashtable;
+import java.util.stream.Collector;
+import java.util.stream.Stream;
+
+import org.osgi.framework.ServiceReference;
+
+public class PropertiesManager {
+ public Stream<String> unflattenStringValues(final Object it) {
+ return String[].class.isInstance(it) ? Stream.of(String[].class.cast(it)) : Stream.of(String.class.cast(it));
+ }
+
+ public <T> Hashtable<String, Object> collectProperties(final ServiceReference<T> ref) {
+ return Stream.of(ref.getPropertyKeys())
+ .filter(it -> !ComponentProperties.INTERCEPTORS_PROPERTY.equals(it))
+ .collect(Collector.of(Hashtable::new, (h, p) -> h.put(p, ref.getProperty(p)), (p1, p2) -> {
+ p1.putAll(p2);
+ return p1;
+ }));
+ }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/ProxiesManager.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/ProxiesManager.java
new file mode 100644
index 0000000..efa5e46
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/ProxiesManager.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime;
+
+import static java.util.Optional.ofNullable;
+import static java.util.stream.Collectors.toList;
+
+import java.util.Collection;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.stream.Stream;
+
+import org.apache.karaf.service.interceptor.impl.runtime.proxy.ProxyFactory;
+import org.apache.karaf.service.interceptor.impl.runtime.registry.InterceptedServiceRegistry;
+import org.apache.karaf.service.interceptor.impl.runtime.registry.InterceptorRegistry;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+
+public class ProxiesManager {
+ private final ProxyFactory proxyFactory;
+ private final PropertiesManager propertiesManager;
+ private final InterceptorRegistry interceptors;
+ private final InterceptedServiceRegistry services;
+
+ private final Map<ServiceReference<?>, ServiceRegistration<?>> registrationPerReference = new ConcurrentHashMap<>();
+ private final Map<ServiceReference<?>, List<Class<?>>> bindingPerReference = new ConcurrentHashMap<>();
+ private final Map<Class<?>, Collection<ServiceReference<?>>> referencesPerBinding = new ConcurrentHashMap<>();
+
+ public ProxiesManager(final InterceptorRegistry interceptorRegistry,
+ final InterceptedServiceRegistry services,
+ final ProxyFactory proxyFactory,
+ final PropertiesManager propertiesManager) {
+ this.interceptors = interceptorRegistry;
+ this.services = services;
+ this.proxyFactory = proxyFactory;
+ this.propertiesManager = propertiesManager;
+ }
+
+ // check out all services not yet proxied which can now be proxied and register the proxy
+ public void onInterceptorAddition(final Class<?> bindingClass) {
+ ofNullable(referencesPerBinding.get(bindingClass))
+ .ifPresent(references -> references.stream()
+ .filter(ref -> !registrationPerReference.containsKey(ref)) // already proxied so skip
+ .filter(ref -> ofNullable(bindingPerReference.get(ref))
+ .map(b -> interceptors.areBindingsAvailable(b.stream()))
+ .orElse(false))
+ .forEach(ref -> registrationPerReference.put(ref, registerProxy(ref))));
+ }
+
+ // remove registered proxies since one of the interceptor is no more available
+ public void onInterceptorRemoval(final Class<?> bindingClass) {
+ ofNullable(referencesPerBinding.get(bindingClass))
+ .ifPresent(references -> references.stream()
+ .filter(registrationPerReference::containsKey)
+ .forEach(ref -> ofNullable(registrationPerReference.remove(ref))
+ .ifPresent(ServiceRegistration::unregister)));
+ }
+
+ public <T> void onInterceptedInstanceAddition(final ServiceReference<T> ref) {
+ final List<Class<?>> bindings = toBindings(ref).collect(toList());
+ bindings.forEach(binding -> referencesPerBinding.computeIfAbsent(binding, k -> new CopyOnWriteArraySet<>()).add(ref));
+ bindingPerReference.put(ref, bindings);
+ if (interceptors.areBindingsAvailable(bindings.stream())) {
+ registrationPerReference.put(ref, registerProxy(ref));
+ }
+ }
+
+ public <T> void onInterceptedInstanceRemoval(final ServiceReference<T> ref) {
+ toBindings(ref).filter(referencesPerBinding::containsKey).forEach(binding -> {
+ final Collection<ServiceReference<?>> refs = referencesPerBinding.get(binding);
+ refs.remove(ref);
+ if (refs.isEmpty()) {
+ referencesPerBinding.remove(binding);
+ }
+ });
+ bindingPerReference.remove(ref);
+ ofNullable(registrationPerReference.remove(ref))
+ .ifPresent(ServiceRegistration::unregister);
+ }
+
+ private <T> Stream<? extends Class<?>> toBindings(final ServiceReference<T> ref) {
+ return services.getBindings(ref);
+ }
+
+ private <T> ServiceRegistration<?> registerProxy(final ServiceReference<T> ref) {
+ final BundleContext context = ref.getBundle().getBundleContext();
+ final Object classProperty = ref.getProperty(Constants.OBJECTCLASS);
+ final List<Class<?>> classes = Stream.of(classProperty)
+ .flatMap(propertiesManager::unflattenStringValues)
+ .map(it -> {
+ try {
+ return context.getBundle().loadClass(it);
+ } catch (final ClassNotFoundException e) {
+ throw new IllegalStateException(e);
+ }
+ })
+ .collect(toList());
+
+ // drop interceptors property to let it be forwarded
+ final Hashtable<String, Object> properties = propertiesManager.collectProperties(ref);
+ final T proxy = proxyFactory.create(
+ ref, classes,
+ interceptors.getInterceptors(bindingPerReference.get(ref)),
+ services.getInterceptorsPerMethod(ref));
+ return context.registerService(classes.stream().map(Class::getName).toArray(String[]::new), proxy, properties);
+ }
+
+ public void stop() {
+ registrationPerReference.values().forEach(ServiceRegistration::unregister);
+ bindingPerReference.clear();
+ referencesPerBinding.clear();
+ registrationPerReference.clear();
+ }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/hook/InterceptedInstancesHooks.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/hook/InterceptedInstancesHooks.java
new file mode 100644
index 0000000..55d4a57
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/hook/InterceptedInstancesHooks.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime.hook;
+
+import static org.apache.karaf.service.interceptor.impl.runtime.ComponentProperties.INTERCEPTORS_PROPERTY;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.hooks.service.EventListenerHook;
+import org.osgi.framework.hooks.service.FindHook;
+import org.osgi.framework.hooks.service.ListenerHook;
+
+public class InterceptedInstancesHooks implements FindHook, EventListenerHook {
+ private final long bundleId;
+
+ public InterceptedInstancesHooks(final long bundleId) {
+ this.bundleId = bundleId;
+ }
+
+ // replaced services are not forward to listeners except the bundle owning the replacer and #0 (optional for the test)
+ @Override
+ public void event(final ServiceEvent event, final Map<BundleContext, Collection<ListenerHook.ListenerInfo>> listeners) {
+ if (isIntercepted(event.getServiceReference())) {
+ listeners.keySet().removeIf(this::isNeitherFrameworkNorSelf);
+ }
+ }
+
+ // remove replaced services to keep only replacements
+ @Override
+ public void find(final BundleContext context, final String name, final String filter,
+ final boolean allServices, final Collection<ServiceReference<?>> references) {
+ if (isNeitherFrameworkNorSelf(context)) {
+ references.removeIf(this::isIntercepted);
+ }
+ }
+
+ private boolean isNeitherFrameworkNorSelf(final BundleContext b) {
+ final long id = b.getBundle().getBundleId();
+ return id != 0 && id != bundleId;
+ }
+
+ private boolean isIntercepted(final ServiceReference<?> serviceReference) {
+ return Boolean.parseBoolean(String.valueOf(serviceReference.getProperty(INTERCEPTORS_PROPERTY)));
+ }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/hook/InterceptorInstance.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/hook/InterceptorInstance.java
new file mode 100644
index 0000000..204b3e2
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/hook/InterceptorInstance.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.karaf.service.interceptor.impl.runtime.hook;
+
+import static java.util.stream.Collectors.toList;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import org.apache.karaf.service.interceptor.api.AroundInvoke;
+import org.apache.karaf.service.interceptor.api.InvocationContext;
+import org.apache.karaf.service.interceptor.impl.runtime.Exceptions;
+import org.apache.karaf.service.interceptor.impl.runtime.PropertiesManager;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+
+public class InterceptorInstance<T> {
+ private final ServiceReference<T> reference;
+ private final BundleContext context;
+ private final Method method;
+ private final Class<?> binding;
+
+ public InterceptorInstance(final ServiceReference<T> reference, final Class<?> binding, final PropertiesManager propertiesManager) {
+ this.reference = reference;
+ this.context = reference.getBundle().getBundleContext();
+ this.method = propertiesManager.unflattenStringValues(reference.getProperty(Constants.OBJECTCLASS))
+ .map(this::findAroundInvoke)
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElse(null);
+ this.binding = binding;
+ }
+
+ public Class<?> getBinding() {
+ return binding;
+ }
+
+ public Object intercept(final InvocationContext invocationContext) throws Exception {
+ final T service = context.getService(reference);
+ if (service == null) {
+ throw new IllegalStateException("'" + reference + "' no more available");
+ }
+ try {
+ return method == null ? invocationContext.proceed() : method.invoke(service, invocationContext);
+ } catch (final InvocationTargetException ite) {
+ return Exceptions.unwrap(ite);
+ } finally {
+ context.ungetService(reference);
+ }
+ }
+
+ private Method findAroundInvoke(final String clazz) {
+ try {
+ final List<Method> interceptingMethods = Stream.of(context.getBundle().loadClass(clazz))
+ .flatMap(c -> Stream.of(c.getMethods()))
+ .filter(m -> m.isAnnotationPresent(AroundInvoke.class))
+ .collect(toList());
+ switch (interceptingMethods.size()) {
+ case 0: // we can add @AroundConstruct later so let's already tolerate that
+ return null;
+ case 1:
+ return interceptingMethods.iterator().next();
+ default:
+ throw new IllegalArgumentException("'" + clazz + "' must have a single @AroundInvoke method, found " + interceptingMethods);
+ }
+ } catch (final ClassNotFoundException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}
+
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/invoker/InterceptorInvocationContext.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/invoker/InterceptorInvocationContext.java
new file mode 100644
index 0000000..ffa4acf
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/invoker/InterceptorInvocationContext.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.karaf.service.interceptor.impl.runtime.invoker;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.karaf.service.interceptor.api.InvocationContext;
+import org.apache.karaf.service.interceptor.impl.runtime.Exceptions;
+import org.apache.karaf.service.interceptor.impl.runtime.hook.InterceptorInstance;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+public class InterceptorInvocationContext<T> implements InvocationContext {
+ private final ServiceReference<T> interceptedReference;
+ private final Method method;
+ private final List<InterceptorInstance<?>> interceptors;
+
+ private T target;
+ private Map<String, Object> contextData;
+ private Object[] parameters;
+ private int index;
+
+ public InterceptorInvocationContext(final ServiceReference<T> reference,
+ final List<InterceptorInstance<?>> interceptors,
+ final Method method, final Object[] parameters) {
+ this.interceptedReference = reference;
+ this.method = method;
+ this.parameters = parameters;
+ this.interceptors = interceptors;
+ }
+
+ @Override
+ public Object proceed() throws Exception {
+ try {
+ if (index < interceptors.size()) {
+ final InterceptorInstance<?> interceptor = interceptors.get(index++);
+ try {
+ return interceptor.intercept(this);
+ } catch (final Exception e) {
+ index--;
+ throw e;
+ }
+ }
+ try {
+ return getMethod().invoke(getTarget(), getParameters());
+ } catch (final InvocationTargetException ite) {
+ return Exceptions.unwrap(ite);
+ }
+ } finally {
+ if (target != null) { // todo: check scope and optimize it?
+ interceptedReference.getBundle().getBundleContext().ungetService(interceptedReference);
+ }
+ }
+ }
+
+ @Override
+ public T getTarget() {
+ final BundleContext context = interceptedReference.getBundle().getBundleContext();
+ target = context.getService(interceptedReference);
+ if (target == null) {
+ throw new IllegalStateException("service no more available (" + interceptedReference + ")");
+ }
+ return target;
+ }
+
+ @Override
+ public Method getMethod() {
+ return method;
+ }
+
+ @Override
+ public Object[] getParameters() {
+ return parameters;
+ }
+
+ @Override
+ public void setParameters(final Object[] parameters) {
+ this.parameters = parameters;
+ }
+
+ @Override
+ public Map<String, Object> getContextData() {
+ if (contextData == null) {
+ contextData = new HashMap<>();
+ }
+ return contextData;
+ }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/invoker/package-info.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/invoker/package-info.java
new file mode 100644
index 0000000..96d06a1
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/invoker/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// this is mainly inspired from OWB
+package org.apache.karaf.service.interceptor.impl.runtime.invoker;
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/AsmProxyFactory.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/AsmProxyFactory.java
new file mode 100644
index 0000000..ff07fd6
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/AsmProxyFactory.java
@@ -0,0 +1,663 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime.proxy;
+
+import static org.objectweb.asm.ClassReader.SKIP_CODE;
+import static org.objectweb.asm.ClassReader.SKIP_DEBUG;
+import static org.objectweb.asm.ClassReader.SKIP_FRAMES;
+import static org.objectweb.asm.Opcodes.AALOAD;
+import static org.objectweb.asm.Opcodes.AASTORE;
+import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
+import static org.objectweb.asm.Opcodes.ACC_PROTECTED;
+import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
+import static org.objectweb.asm.Opcodes.ACC_STATIC;
+import static org.objectweb.asm.Opcodes.ACC_SUPER;
+import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
+import static org.objectweb.asm.Opcodes.ACC_VARARGS;
+import static org.objectweb.asm.Opcodes.ACONST_NULL;
+import static org.objectweb.asm.Opcodes.ALOAD;
+import static org.objectweb.asm.Opcodes.ANEWARRAY;
+import static org.objectweb.asm.Opcodes.ARETURN;
+import static org.objectweb.asm.Opcodes.ASM7;
+import static org.objectweb.asm.Opcodes.ASTORE;
+import static org.objectweb.asm.Opcodes.ATHROW;
+import static org.objectweb.asm.Opcodes.BIPUSH;
+import static org.objectweb.asm.Opcodes.CHECKCAST;
+import static org.objectweb.asm.Opcodes.DLOAD;
+import static org.objectweb.asm.Opcodes.DRETURN;
+import static org.objectweb.asm.Opcodes.DUP;
+import static org.objectweb.asm.Opcodes.FLOAD;
+import static org.objectweb.asm.Opcodes.FRETURN;
+import static org.objectweb.asm.Opcodes.GETFIELD;
+import static org.objectweb.asm.Opcodes.GETSTATIC;
+import static org.objectweb.asm.Opcodes.ICONST_0;
+import static org.objectweb.asm.Opcodes.ICONST_1;
+import static org.objectweb.asm.Opcodes.ICONST_2;
+import static org.objectweb.asm.Opcodes.ICONST_3;
+import static org.objectweb.asm.Opcodes.ICONST_4;
+import static org.objectweb.asm.Opcodes.ICONST_5;
+import static org.objectweb.asm.Opcodes.IFEQ;
+import static org.objectweb.asm.Opcodes.ILOAD;
+import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
+import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
+import static org.objectweb.asm.Opcodes.INVOKESTATIC;
+import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
+import static org.objectweb.asm.Opcodes.IRETURN;
+import static org.objectweb.asm.Opcodes.LLOAD;
+import static org.objectweb.asm.Opcodes.LRETURN;
+import static org.objectweb.asm.Opcodes.NEW;
+import static org.objectweb.asm.Opcodes.POP;
+import static org.objectweb.asm.Opcodes.PUTFIELD;
+import static org.objectweb.asm.Opcodes.RETURN;
+import static org.objectweb.asm.Opcodes.SIPUSH;
+import static org.objectweb.asm.Opcodes.V10;
+import static org.objectweb.asm.Opcodes.V11;
+import static org.objectweb.asm.Opcodes.V12;
+import static org.objectweb.asm.Opcodes.V13;
+import static org.objectweb.asm.Opcodes.V1_8;
+import static org.objectweb.asm.Opcodes.V9;
+
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Type;
+
+// forked from OWB
+public class AsmProxyFactory {
+ private static final Method[] EMPTY_METHODS = new Method[0];
+
+ private static final String FIELD_INTERCEPTOR_HANDLER = "karafInterceptorProxyHandler";
+ private static final String FIELD_INTERCEPTED_METHODS = "karafInterceptorProxyMethods";
+
+ public <T> T create(final Class<?> clazz, final InterceptorHandler handler) {
+ try {
+ final T proxy = (T) clazz.getConstructor().newInstance();
+ final Field invocationHandlerField = clazz.getDeclaredField(FIELD_INTERCEPTOR_HANDLER);
+ invocationHandlerField.setAccessible(true);
+ invocationHandlerField.set(proxy, handler);
+ return proxy;
+ } catch (final IllegalAccessException | NoSuchFieldException | NoSuchMethodException | InstantiationException e) {
+ throw new IllegalStateException(e);
+ } catch (final InvocationTargetException ite) {
+ throw new IllegalStateException(ite.getTargetException());
+ }
+ }
+
+ public <T> Class<T> createProxyClass(final ProxyFactory.ProxyClassLoader classLoader,
+ final String proxyClassName, final Class<?>[] classesToProxy,
+ final Method[] interceptedMethods) {
+ try {
+ return (Class<T>) Class.forName(proxyClassName, true, classLoader);
+ } catch (final ClassNotFoundException cnfe) {
+ return doCreateProxyClass(classLoader, proxyClassName, classesToProxy, interceptedMethods);
+ }
+ }
+
+ private <T> Class<T> doCreateProxyClass(final ProxyFactory.ProxyClassLoader classLoader, final String proxyClassName,
+ final Class<?>[] classesToProxy, final Method[] interceptedMethods) {
+ final String proxyClassFileName = proxyClassName.replace('.', '/');
+ final byte[] proxyBytes = generateProxy(classesToProxy, proxyClassFileName, sortOutDuplicateMethods(interceptedMethods));
+ final Class<T> proxyCLass = classLoader.getOrRegister(proxyClassName, proxyBytes, classesToProxy[0].getPackage(), classesToProxy[0].getProtectionDomain());
+ try {
+ final Field interceptedMethodsField = proxyCLass.getDeclaredField(FIELD_INTERCEPTED_METHODS);
+ interceptedMethodsField.setAccessible(true);
+ interceptedMethodsField.set(null, interceptedMethods);
+ } catch (final Exception e) {
+ throw new IllegalStateException(e);
+ }
+ return proxyCLass;
+ }
+
+ private Method[] sortOutDuplicateMethods(final Method[] methods) {
+ if (methods == null || methods.length == 0) {
+ return null;
+ }
+
+ final List<Method> duplicates = new ArrayList<>();
+ for (final Method outer : methods) {
+ for (final Method inner : methods) {
+ if (inner != outer
+ && hasSameSignature(outer, inner)
+ && !(duplicates.contains(outer) || duplicates.contains(inner))) {
+ duplicates.add(inner);
+ }
+ }
+ }
+
+ final List<Method> outsorted = new ArrayList<>(Arrays.asList(methods));
+ outsorted.removeAll(duplicates);
+ return outsorted.toArray(EMPTY_METHODS);
+ }
+
+ private boolean hasSameSignature(Method a, Method b) {
+ return a.getName().equals(b.getName())
+ && a.getReturnType().equals(b.getReturnType())
+ && Arrays.equals(a.getParameterTypes(), b.getParameterTypes());
+ }
+
+ private void createConstructor(final ClassWriter cw, final String proxyClassFileName, final Class<?> classToProxy,
+ final String classFileName) {
+ Constructor superDefaultCt;
+ String parentClassFileName = classFileName;
+ String descriptor = "()V";
+
+ try {
+ if (classToProxy.isInterface()) {
+ parentClassFileName = Type.getInternalName(Object.class);
+ superDefaultCt = Object.class.getConstructor(null);
+ descriptor = Type.getConstructorDescriptor(superDefaultCt);
+ }
+ } catch (final NoSuchMethodException nsme) {
+ // no worries
+ }
+
+ MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", descriptor, null, null);
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitMethodInsn(INVOKESPECIAL, parentClassFileName, "<init>", descriptor, false);
+
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitInsn(ACONST_NULL);
+ mv.visitFieldInsn(PUTFIELD, proxyClassFileName, FIELD_INTERCEPTOR_HANDLER, Type.getDescriptor(InterceptorHandler.class));
+
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(-1, -1);
+ mv.visitEnd();
+ }
+
+ private byte[] generateProxy(final Class<?>[] classesToProxy, final String proxyClassFileName, final Method[] interceptedMethods) {
+ final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
+ final String classFileName = classesToProxy[0].getName().replace('.', '/');
+
+ final String[] interfaces = Stream.of(classesToProxy)
+ .filter(Class::isInterface)
+ .map(Type::getInternalName)
+ .toArray(String[]::new);
+ final String superClassName;
+ if (interfaces.length == classesToProxy.length) {
+ superClassName = Type.getInternalName(Object.class);
+ } else {
+ superClassName = Type.getInternalName(classesToProxy[0]);
+ }
+
+ cw.visit(findJavaVersion(classesToProxy[0]), ACC_PUBLIC + ACC_SUPER + ACC_SYNTHETIC, proxyClassFileName, null, superClassName, interfaces);
+ cw.visitSource(classFileName + ".java", null);
+ createInstanceVariables(cw);
+ createConstructor(cw, proxyClassFileName, classesToProxy[0], classFileName);
+ if (interceptedMethods != null) {
+ delegateInterceptedMethods(cw, proxyClassFileName, classesToProxy[0], interceptedMethods);
+ }
+ return cw.toByteArray();
+ }
+
+ private void createInstanceVariables(final ClassWriter cw) {
+ cw.visitField(ACC_PRIVATE,
+ FIELD_INTERCEPTOR_HANDLER, Type.getDescriptor(InterceptorHandler.class), null, null).visitEnd();
+ cw.visitField(ACC_PRIVATE | ACC_STATIC,
+ FIELD_INTERCEPTED_METHODS, Type.getDescriptor(Method[].class), null, null).visitEnd();
+ }
+
+ private void delegateInterceptedMethods(final ClassWriter cw,
+ final String proxyClassFileName, final Class<?> classToProxy,
+ final Method[] interceptedMethods) {
+ for (int i = 0; i < interceptedMethods.length; i++) {
+ if (!unproxyableMethod(interceptedMethods[i])) {
+ generateInterceptorHandledMethod(cw, interceptedMethods[i], i, classToProxy, proxyClassFileName);
+ }
+ }
+ }
+
+ private void generateInterceptorHandledMethod(final ClassWriter cw, final Method method, final int methodIndex,
+ final Class<?> classToProxy, final String proxyClassFileName) {
+ if ("<init>".equals(method.getName())) {
+ return;
+ }
+
+ final Class<?> returnType = method.getReturnType();
+ final Class<?>[] parameterTypes = method.getParameterTypes();
+ final Class<?>[] exceptionTypes = method.getExceptionTypes();
+ final int modifiers = method.getModifiers();
+ if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers)) {
+ throw new IllegalStateException("It's not possible to proxy a final or static method: " + classToProxy.getName() + " " + method.getName());
+ }
+
+ // push the method definition
+ final int modifier = modifiers & (ACC_PUBLIC | ACC_PROTECTED | ACC_VARARGS);
+
+ final MethodVisitor mv = cw.visitMethod(modifier, method.getName(), Type.getMethodDescriptor(method), null, null);
+ mv.visitCode();
+ // push try/catch block, to catch declared exceptions, and to catch java.lang.Throwable
+ final Label l0 = new Label();
+ final Label l1 = new Label();
+ final Label l2 = new Label();
+
+ if (exceptionTypes.length > 0) {
+ mv.visitTryCatchBlock(l0, l1, l2, "java/lang/reflect/InvocationTargetException");
+ }
+
+ // push try code
+ mv.visitLabel(l0);
+ final String classNameToOverride = method.getDeclaringClass().getName().replace('.', '/');
+ mv.visitLdcInsn(Type.getType("L" + classNameToOverride + ";"));
+
+ // the following code generates the bytecode for this line of Java:
+ // Method method = <proxy>.class.getMethod("add", new Class[] { <array of function argument classes> });
+
+ // get the method name to invoke, and push to stack
+ mv.visitLdcInsn(method.getName());
+
+ // create the Class[]
+ createArrayDefinition(mv, parameterTypes.length, Class.class);
+
+ int length = 1;
+
+ // push parameters into array
+ for (int i = 0; i < parameterTypes.length; i++) {
+ // keep copy of array on stack
+ mv.visitInsn(DUP);
+
+ final Class<?> parameterType = parameterTypes[i];
+
+ // push number onto stack
+ pushIntOntoStack(mv, i);
+
+ if (parameterType.isPrimitive()) {
+ String wrapperType = getWrapperType(parameterType);
+ mv.visitFieldInsn(GETSTATIC, wrapperType, "TYPE", "Ljava/lang/Class;");
+ } else {
+ mv.visitLdcInsn(Type.getType(parameterType));
+ }
+
+ mv.visitInsn(AASTORE);
+
+ if (Long.TYPE.equals(parameterType) || Double.TYPE.equals(parameterType)) {
+ length += 2;
+ } else {
+ length++;
+ }
+ }
+
+ // the following code generates bytecode equivalent to:
+ // return ((<returntype>) invocationHandler.invoke(this, {methodIndex}, new Object[] { <function arguments }))[.<primitive>Value()];
+
+ final Label l4 = new Label();
+ mv.visitLabel(l4);
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitFieldInsn(GETFIELD, proxyClassFileName, FIELD_INTERCEPTOR_HANDLER, Type.getDescriptor(InterceptorHandler.class));
+ mv.visitFieldInsn(GETSTATIC, proxyClassFileName, FIELD_INTERCEPTED_METHODS, Type.getDescriptor(Method[].class));
+ if (methodIndex < 128) {
+ mv.visitIntInsn(BIPUSH, methodIndex);
+ } else if (methodIndex < 32267) {
+ mv.visitIntInsn(SIPUSH, methodIndex);
+ } else {
+ throw new IllegalStateException("Sorry, we only support Classes with 2^15 methods...");
+ }
+
+ mv.visitInsn(AALOAD);
+ pushMethodParameterArray(mv, parameterTypes);
+ mv.visitMethodInsn(INVOKEINTERFACE, Type.getInternalName(InterceptorHandler.class), "invoke",
+ "(Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;", true);
+ mv.visitTypeInsn(CHECKCAST, getCastType(returnType));
+ if (returnType.isPrimitive() && (!Void.TYPE.equals(returnType))) {
+ // get the primitive value
+ mv.visitMethodInsn(INVOKEVIRTUAL, getWrapperType(returnType), getPrimitiveMethod(returnType),
+ "()" + Type.getDescriptor(returnType), false);
+ }
+
+ mv.visitLabel(l1);
+ if (!Void.TYPE.equals(returnType)) {
+ mv.visitInsn(getReturnInsn(returnType));
+ } else {
+ mv.visitInsn(POP);
+ mv.visitInsn(RETURN);
+ }
+
+ // catch InvocationTargetException
+ if (exceptionTypes.length > 0) {
+ mv.visitLabel(l2);
+ mv.visitVarInsn(ASTORE, length);
+
+ Label l5 = new Label();
+ mv.visitLabel(l5);
+
+ for (int i = 0; i < exceptionTypes.length; i++) {
+ Class<?> exceptionType = exceptionTypes[i];
+
+ mv.visitLdcInsn(Type.getType("L" + exceptionType.getCanonicalName().replace('.', '/') + ";"));
+ mv.visitVarInsn(ALOAD, length);
+ mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/reflect/InvocationTargetException", "getCause",
+ "()Ljava/lang/Throwable;", false);
+ mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
+ mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "equals", "(Ljava/lang/Object;)Z", false);
+
+ Label l6 = new Label();
+ mv.visitJumpInsn(IFEQ, l6);
+
+ Label l7 = new Label();
+ mv.visitLabel(l7);
+
+ mv.visitVarInsn(ALOAD, length);
+ mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/reflect/InvocationTargetException", "getCause",
+ "()Ljava/lang/Throwable;", false);
+ mv.visitTypeInsn(CHECKCAST, getCastType(exceptionType));
+ mv.visitInsn(ATHROW);
+ mv.visitLabel(l6);
+
+ if (i == (exceptionTypes.length - 1)) {
+ mv.visitTypeInsn(NEW, "java/lang/reflect/UndeclaredThrowableException");
+ mv.visitInsn(DUP);
+ mv.visitVarInsn(ALOAD, length);
+ mv.visitMethodInsn(INVOKESPECIAL, "java/lang/reflect/UndeclaredThrowableException", "<init>",
+ "(Ljava/lang/Throwable;)V", false);
+ mv.visitInsn(ATHROW);
+ }
+ }
+ }
+
+ mv.visitMaxs(0, 0);
+ mv.visitEnd();
+ }
+
+ private int findJavaVersion(final Class<?> from) {
+ final String resource = from.getName().replace('.', '/') + ".class";
+ try (final InputStream stream = from.getClassLoader().getResourceAsStream(resource)) {
+ if (stream == null) {
+ return V1_8;
+ }
+ final ClassReader reader = new ClassReader(stream);
+ final VersionVisitor visitor = new VersionVisitor();
+ reader.accept(visitor, SKIP_DEBUG + SKIP_CODE + SKIP_FRAMES);
+ if (visitor.version != 0) {
+ return visitor.version;
+ }
+ } catch (final Exception e) {
+ // no-op
+ }
+ // mainly for JVM classes - outside the classloader, find to fallback on the JVM version
+ final String javaVersionProp = System.getProperty("java.version", "1.8");
+ if (javaVersionProp.startsWith("1.8")) {
+ return V1_8;
+ } else if (javaVersionProp.startsWith("9") || javaVersionProp.startsWith("1.9")) {
+ return V9;
+ } else if (javaVersionProp.startsWith("10")) {
+ return V10;
+ } else if (javaVersionProp.startsWith("11")) {
+ return V11;
+ } else if (javaVersionProp.startsWith("12")) {
+ return V12;
+ } else if (javaVersionProp.startsWith("13")) {
+ return V13;
+ }
+ try {
+ final int i = Integer.parseInt(javaVersionProp);
+ if (i > 13) {
+ return V13 + (i - 13);
+ }
+ return V1_8;
+ } catch (final NumberFormatException nfe) {
+ return V1_8;
+ }
+ }
+
+ private boolean unproxyableMethod(final Method delegatedMethod) {
+ final int modifiers = delegatedMethod.getModifiers();
+ return (modifiers & (Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL | Modifier.NATIVE)) > 0 ||
+ "finalize".equals(delegatedMethod.getName()) || delegatedMethod.isBridge();
+ }
+
+ /**
+ * @return the wrapper type for a primitive, e.g. java.lang.Integer for int
+ */
+ private String getWrapperType(Class<?> type) {
+ if (Integer.TYPE.equals(type)) {
+ return Integer.class.getCanonicalName().replace('.', '/');
+ } else if (Boolean.TYPE.equals(type)) {
+ return Boolean.class.getCanonicalName().replace('.', '/');
+ } else if (Character.TYPE.equals(type)) {
+ return Character.class.getCanonicalName().replace('.', '/');
+ } else if (Byte.TYPE.equals(type)) {
+ return Byte.class.getCanonicalName().replace('.', '/');
+ } else if (Short.TYPE.equals(type)) {
+ return Short.class.getCanonicalName().replace('.', '/');
+ } else if (Float.TYPE.equals(type)) {
+ return Float.class.getCanonicalName().replace('.', '/');
+ } else if (Long.TYPE.equals(type)) {
+ return Long.class.getCanonicalName().replace('.', '/');
+ } else if (Double.TYPE.equals(type)) {
+ return Double.class.getCanonicalName().replace('.', '/');
+ } else if (Void.TYPE.equals(type)) {
+ return Void.class.getCanonicalName().replace('.', '/');
+ }
+
+ throw new IllegalStateException("Type: " + type.getCanonicalName() + " is not a primitive type");
+ }
+
+ /**
+ * Returns the appropriate bytecode instruction to load a value from a variable to the stack
+ *
+ * @param type Type to load
+ * @return Bytecode instruction to use
+ */
+ private int getVarInsn(Class<?> type) {
+ if (type.isPrimitive()) {
+ if (Integer.TYPE.equals(type)) {
+ return ILOAD;
+ } else if (Boolean.TYPE.equals(type)) {
+ return ILOAD;
+ } else if (Character.TYPE.equals(type)) {
+ return ILOAD;
+ } else if (Byte.TYPE.equals(type)) {
+ return ILOAD;
+ } else if (Short.TYPE.equals(type)) {
+ return ILOAD;
+ } else if (Float.TYPE.equals(type)) {
+ return FLOAD;
+ } else if (Long.TYPE.equals(type)) {
+ return LLOAD;
+ } else if (Double.TYPE.equals(type)) {
+ return DLOAD;
+ }
+ }
+
+ throw new IllegalStateException("Type: " + type.getCanonicalName() + " is not a primitive type");
+ }
+
+ /**
+ * Invokes the most appropriate bytecode instruction to put a number on the stack
+ *
+ * @param mv
+ * @param i
+ */
+ private void pushIntOntoStack(final MethodVisitor mv, final int i) {
+ if (i == 0) {
+ mv.visitInsn(ICONST_0);
+ } else if (i == 1) {
+ mv.visitInsn(ICONST_1);
+ } else if (i == 2) {
+ mv.visitInsn(ICONST_2);
+ } else if (i == 3) {
+ mv.visitInsn(ICONST_3);
+ } else if (i == 4) {
+ mv.visitInsn(ICONST_4);
+ } else if (i == 5) {
+ mv.visitInsn(ICONST_5);
+ } else if (i > 5 && i <= 255) {
+ mv.visitIntInsn(BIPUSH, i);
+ } else {
+ mv.visitIntInsn(SIPUSH, i);
+ }
+ }
+
+ /**
+ * Gets the appropriate bytecode instruction for RETURN, according to what type we need to return
+ *
+ * @param type Type the needs to be returned
+ * @return The matching bytecode instruction
+ */
+ private int getReturnInsn(final Class<?> type) {
+ if (type.isPrimitive()) {
+ if (Void.TYPE.equals(type)) {
+ return RETURN;
+ }
+ if (Integer.TYPE.equals(type)) {
+ return IRETURN;
+ } else if (Boolean.TYPE.equals(type)) {
+ return IRETURN;
+ } else if (Character.TYPE.equals(type)) {
+ return IRETURN;
+ } else if (Byte.TYPE.equals(type)) {
+ return IRETURN;
+ } else if (Short.TYPE.equals(type)) {
+ return IRETURN;
+ } else if (Float.TYPE.equals(type)) {
+ return FRETURN;
+ } else if (Long.TYPE.equals(type)) {
+ return LRETURN;
+ } else if (Double.TYPE.equals(type)) {
+ return DRETURN;
+ }
+ }
+ return ARETURN;
+ }
+
+ /**
+ * Gets the string to use for CHECKCAST instruction, returning the correct value for any type, including primitives and arrays
+ *
+ * @param returnType The type to cast to with CHECKCAST
+ * @return CHECKCAST parameter
+ */
+ private String getCastType(Class<?> returnType) {
+ if (returnType.isPrimitive()) {
+ return getWrapperType(returnType);
+ } else {
+ return Type.getInternalName(returnType);
+ }
+ }
+
+ /**
+ * Returns the name of the Java method to call to get the primitive value from an Object - e.g. intValue for java.lang.Integer
+ *
+ * @param type Type whose primitive method we want to lookup
+ * @return The name of the method to use
+ */
+ private String getPrimitiveMethod(final Class<?> type) {
+ if (Integer.TYPE.equals(type)) {
+ return "intValue";
+ } else if (Boolean.TYPE.equals(type)) {
+ return "booleanValue";
+ } else if (Character.TYPE.equals(type)) {
+ return "charValue";
+ } else if (Byte.TYPE.equals(type)) {
+ return "byteValue";
+ } else if (Short.TYPE.equals(type)) {
+ return "shortValue";
+ } else if (Float.TYPE.equals(type)) {
+ return "floatValue";
+ } else if (Long.TYPE.equals(type)) {
+ return "longValue";
+ } else if (Double.TYPE.equals(type)) {
+ return "doubleValue";
+ }
+
+ throw new IllegalStateException("Type: " + type.getCanonicalName() + " is not a primitive type");
+ }
+
+ private void generateReturn(final MethodVisitor mv, final Method delegatedMethod) {
+ final Class<?> returnType = delegatedMethod.getReturnType();
+ mv.visitInsn(getReturnInsn(returnType));
+ }
+
+ /**
+ * Create an Object[] parameter which contains all the parameters of the currently invoked method
+ * and store this array for use in the call stack.
+ *
+ * @param mv
+ * @param parameterTypes
+ */
+ private void pushMethodParameterArray(MethodVisitor mv, Class<?>[] parameterTypes) {
+ // need to construct the array of objects passed in
+ // create the Object[]
+ createArrayDefinition(mv, parameterTypes.length, Object.class);
+
+ int index = 1;
+ for (int i = 0; i < parameterTypes.length; i++) {
+ // keep copy of array on stack
+ mv.visitInsn(DUP);
+
+ final Class<?> parameterType = parameterTypes[i];
+ pushIntOntoStack(mv, i);
+
+ if (parameterType.isPrimitive()) {
+ final String wrapperType = getWrapperType(parameterType);
+ mv.visitVarInsn(getVarInsn(parameterType), index);
+ mv.visitMethodInsn(INVOKESTATIC, wrapperType, "valueOf",
+ "(" + Type.getDescriptor(parameterType) + ")L" + wrapperType + ";", false);
+ mv.visitInsn(AASTORE);
+
+ if (Long.TYPE.equals(parameterType) || Double.TYPE.equals(parameterType)) {
+ index += 2;
+ } else {
+ index++;
+ }
+ } else {
+ mv.visitVarInsn(ALOAD, index);
+ mv.visitInsn(AASTORE);
+ index++;
+ }
+ }
+ }
+
+ private void createArrayDefinition(final MethodVisitor mv, final int size, final Class<?> type) {
+ if (size < 0) {
+ throw new IllegalStateException("Array size cannot be less than zero");
+ }
+ pushIntOntoStack(mv, size);
+ mv.visitTypeInsn(ANEWARRAY, type.getCanonicalName().replace('.', '/'));
+ }
+
+
+ private static class VersionVisitor extends ClassVisitor {
+ private int version;
+
+ private VersionVisitor() {
+ super(ASM7);
+ }
+
+ @Override
+ public void visit(final int version, final int access, final String name,
+ final String signature, final String superName, final String[] interfaces) {
+ this.version = version;
+ }
+ }
+
+ public interface InterceptorHandler {
+ Object invoke(Method method, Object[] args) throws Exception;
+ }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/ProxyFactory.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/ProxyFactory.java
new file mode 100644
index 0000000..0fe2479
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/ProxyFactory.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.karaf.service.interceptor.impl.runtime.proxy;
+
+import static java.util.Collections.emptyList;
+import static java.util.Optional.ofNullable;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Proxy;
+import java.net.URL;
+import java.security.ProtectionDomain;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Stream;
+
+import org.apache.karaf.service.interceptor.impl.runtime.hook.InterceptorInstance;
+import org.apache.karaf.service.interceptor.impl.runtime.invoker.InterceptorInvocationContext;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+
+public class ProxyFactory {
+
+ private static final Class<?>[] EMPTY_CLASSES = new Class<?>[0];
+
+ public <T> T create(final ServiceReference<T> ref, final List<Class<?>> classes,
+ final List<InterceptorInstance<?>> interceptors,
+ final Map<Method, List<Class<?>>> interceptorsPerMethod) {
+ if (classes.isEmpty()) {
+ throw new IllegalArgumentException("Can't proxy an empty list of type: " + ref);
+ }
+
+ final Map<Method, List<InterceptorInstance<?>>> interceptorInstancePerMethod = interceptorsPerMethod.entrySet().stream()
+ .collect(toMap(Map.Entry::getKey, m -> m.getValue().stream()
+ .map(binding -> interceptors.stream().filter(i -> i.getBinding() == binding).findFirst().orElse(null))
+ .collect(toList())));
+
+ final ProxyClassLoader loader = new ProxyClassLoader(Thread.currentThread().getContextClassLoader(), ref.getBundle());
+ if (classes.stream().allMatch(Class::isInterface)) {
+ final Object proxyInstance = Proxy.newProxyInstance(
+ loader,
+ classes.toArray(EMPTY_CLASSES),
+ (proxy, method, args) -> doInvoke(ref, method, args, interceptorInstancePerMethod));
+ return (T) proxyInstance;
+ }
+ final AsmProxyFactory asm = new AsmProxyFactory();
+ final Class<?> proxyClass = asm.createProxyClass(
+ loader,
+ getProxyClassName(classes),
+ classes.stream().sorted(this::compareClasses).toArray(Class<?>[]::new),
+ findInterceptedMethods(classes));
+ return asm.create(proxyClass, (method, args) -> doInvoke(ref, method, args, interceptorInstancePerMethod));
+ }
+
+ private <T> Object doInvoke(final ServiceReference<T> ref,
+ final Method method, final Object[] args,
+ final Map<Method, List<InterceptorInstance<?>>> interceptorsPerMethod) throws Exception {
+ final List<InterceptorInstance<?>> methodInterceptors = interceptorsPerMethod.getOrDefault(method, emptyList());
+ return new InterceptorInvocationContext<>(ref, methodInterceptors, method, args).proceed();
+ }
+
+ private int compareClasses(final Class<?> c1, final Class<?> c2) {
+ if (c1 == c2) {
+ return 0;
+ }
+ if (c1.isAssignableFrom(c2)) {
+ return 1;
+ }
+ if (c2.isAssignableFrom(c1)) {
+ return -1;
+ }
+ if (c1.isInterface() && !c2.isInterface()) {
+ return 1;
+ }
+ if (c2.isInterface() && !c1.isInterface()) {
+ return -1;
+ }
+ if (!c1.isInterface() && !c2.isInterface()) {
+ throw new IllegalArgumentException("No common class between " + c1 + " and " + c2);
+ }
+ return c1.getName().compareTo(c2.getName()); // just to be deterministic
+ }
+
+ private Method[] findInterceptedMethods(final List<Class<?>> classes) {
+ return classes.stream()
+ .flatMap(c -> c.isInterface() ? Stream.of(c.getMethods()) : findMethods(c))
+ .distinct()
+ .filter(method -> Modifier.isPublic(method.getModifiers())) // todo: enable protected? not that scr friendly but doable
+ .toArray(Method[]::new);
+ }
+
+ private Stream<Method> findMethods(final Class<?> clazz) {
+ return clazz == null || Object.class == clazz ?
+ Stream.empty() :
+ Stream.concat(Stream.of(clazz.getDeclaredMethods()), findMethods(clazz.getSuperclass()));
+ }
+
+ private String getProxyClassName(final List<Class<?>> classes) {
+ return classes.iterator().next().getName() + "$$KarafInterceptorProxy" +
+ classes.stream().skip(1).map(c -> c.getName().replace(".", "_").replace("$", "")).collect(joining("__"));
+ }
+
+ static class ProxyClassLoader extends ClassLoader {
+ private final Bundle bundle;
+ private final Map<String, Class<?>> classes = new ConcurrentHashMap<>();
+
+ ProxyClassLoader(final ClassLoader parent, final Bundle bundle) {
+ super(parent);
+ this.bundle = bundle;
+ }
+
+ @Override
+ protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
+ final Class<?> clazz = classes.get(name);
+ if (clazz != null) {
+ return clazz;
+ }
+ if (bundle != null) {
+ try {
+ return bundle.loadClass(name);
+ } catch (final ClassNotFoundException cnfe) {
+ if (name != null && name.startsWith("org.apache.karaf.service.interceptor.")) {
+ return getClass().getClassLoader().loadClass(name);
+ }
+ throw cnfe;
+ }
+ }
+ return super.loadClass(name, resolve);
+ }
+
+ @Override
+ public URL getResource(final String name) {
+ return bundle.getResource(name);
+ }
+
+ @Override
+ public Enumeration<URL> getResources(final String name) throws IOException {
+ return bundle.getResources(name);
+ }
+
+ @Override
+ public InputStream getResourceAsStream(final String name) {
+ return ofNullable(getResource(name)).map(u -> {
+ try {
+ return u.openStream();
+ } catch (final IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }).orElse(null);
+ }
+
+ public <T> Class<T> getOrRegister(final String proxyClassName, final byte[] proxyBytes,
+ final Package pck, final ProtectionDomain protectionDomain) {
+ final String key = proxyClassName.replace('/', '.');
+ Class<?> existing = classes.get(key);
+ if (existing == null) {
+ synchronized (this) {
+ existing = classes.get(key);
+ if (existing == null) {
+ definePackageFor(pck, protectionDomain);
+ existing = super.defineClass(proxyClassName, proxyBytes, 0, proxyBytes.length);
+ resolveClass(existing);
+ classes.put(key, existing);
+ }
+ }
+ }
+ return (Class<T>) existing;
+ }
+
+ private void definePackageFor(final Package model, final ProtectionDomain protectionDomain) {
+ if (model == null) {
+ return;
+ }
+ if (getPackage(model.getName()) == null) {
+ if (model.isSealed() && protectionDomain != null &&
+ protectionDomain.getCodeSource() != null &&
+ protectionDomain.getCodeSource().getLocation() != null) {
+ definePackage(
+ model.getName(),
+ model.getSpecificationTitle(),
+ model.getSpecificationVersion(),
+ model.getSpecificationVendor(),
+ model.getImplementationTitle(),
+ model.getImplementationVersion(),
+ model.getImplementationVendor(),
+ protectionDomain.getCodeSource().getLocation());
+ } else {
+ definePackage(
+ model.getName(),
+ model.getSpecificationTitle(),
+ model.getSpecificationVersion(),
+ model.getSpecificationVendor(),
+ model.getImplementationTitle(),
+ model.getImplementationVersion(),
+ model.getImplementationVendor(),
+ null);
+ }
+ }
+ }
+ }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/registry/InterceptedServiceRegistry.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/registry/InterceptedServiceRegistry.java
new file mode 100644
index 0000000..f9da914
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/registry/InterceptedServiceRegistry.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.karaf.service.interceptor.impl.runtime.registry;
+
+import static java.util.Optional.ofNullable;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import org.apache.karaf.service.interceptor.api.InterceptorBinding;
+import org.apache.karaf.service.interceptor.impl.runtime.PropertiesManager;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+
+public class InterceptedServiceRegistry implements ServiceListener {
+ private final PropertiesManager propertiesManager;
+ private final Consumer<ServiceReference<?>> onServiceAddition;
+ private final Consumer<ServiceReference<?>> onServiceRemoval;
+ private final Map<ServiceReference<?>, RegistrationState> references = new ConcurrentHashMap<>();
+
+ public InterceptedServiceRegistry(final Consumer<ServiceReference<?>> onServiceAddition,
+ final Consumer<ServiceReference<?>> onServiceRemoval,
+ final PropertiesManager propertiesManager) {
+ this.onServiceAddition = onServiceAddition;
+ this.onServiceRemoval = onServiceRemoval;
+ this.propertiesManager = propertiesManager;
+ }
+
+ @Override
+ public void serviceChanged(final ServiceEvent serviceEvent) {
+ final ServiceReference<?> ref = serviceEvent.getServiceReference();
+ switch (serviceEvent.getType()) {
+ case ServiceEvent.REGISTERED:
+ doRegister(ref);
+ break;
+ case ServiceEvent.MODIFIED_ENDMATCH:
+ case ServiceEvent.UNREGISTERING:
+ doRemove(ref);
+ break;
+ case ServiceEvent.MODIFIED:
+ ofNullable(references.get(ref))
+ .filter(reg -> didChange(ref, reg))
+ .ifPresent(reg -> {
+ doRemove(ref);
+ doRegister(ref);
+ });
+ default:
+ }
+ }
+
+ private boolean didChange(final ServiceReference<?> ref, final RegistrationState reg) {
+ return !reg.registrationProperties.equals(propertiesManager.collectProperties(ref)) ||
+ !reg.bindingsPerMethod.equals(computeBindings(ref));
+ }
+
+ private void doRegister(final ServiceReference<?> ref) {
+ references.put(ref, new RegistrationState(propertiesManager.collectProperties(ref), computeBindings(ref)));
+ onServiceAddition.accept(ref);
+ }
+
+ private void doRemove(final ServiceReference<?> ref) {
+ onServiceRemoval.accept(ref);
+ references.remove(ref);
+ }
+
+ private Map<Method, List<Class<?>>> computeBindings(final ServiceReference<?> ref) {
+ final List<Class<?>> types = propertiesManager.unflattenStringValues(ref.getProperty(Constants.OBJECTCLASS))
+ .map(it -> {
+ try {
+ return ref.getBundle().loadClass(it);
+ } catch (final ClassNotFoundException e) {
+ throw new IllegalStateException(e);
+ }
+ })
+ .distinct()
+ .collect(toList());
+ final Collection<Annotation> globalInterceptors = types.stream()
+ .flatMap(type -> Stream.of(type.getAnnotations()))
+ .filter(methodAnnotation -> methodAnnotation.annotationType().isAnnotationPresent(InterceptorBinding.class))
+ .distinct()
+ .collect(toList());
+ return types.stream()
+ .flatMap(type -> Stream.of(type.getMethods()))
+ .collect(toMap(identity(), m -> Stream.concat(
+ globalInterceptors.stream(),
+ Stream.of(m.getAnnotations()))
+ .filter(methodAnnotation -> methodAnnotation.annotationType().isAnnotationPresent(InterceptorBinding.class))
+ .distinct()
+ .map(Annotation::annotationType) // todo: keep Annotation with values
+ .collect(toList())));
+ }
+
+ public <T> Stream<Class<?>> getBindings(final ServiceReference<T> ref) {
+ return ofNullable(references.get(ref))
+ .map(reg -> reg.bindingsPerMethod.values().stream().flatMap(Collection::stream).distinct())
+ .orElseGet(Stream::empty);
+ }
+
+ public <T> Map<Method, List<Class<?>>> getInterceptorsPerMethod(final ServiceReference<T> ref) {
+ return ofNullable(references.get(ref))
+ .map(reg -> reg.bindingsPerMethod)
+ .orElseGet(Collections::emptyMap);
+ }
+
+ private static class RegistrationState {
+ private final Hashtable<String, Object> registrationProperties;
+ private final Map<Method, List<Class<?>>> bindingsPerMethod;
+
+ private RegistrationState(final Hashtable<String, Object> registrationProperties,
+ final Map<Method, List<Class<?>>> bindingsPerMethod) {
+ this.registrationProperties = registrationProperties;
+ this.bindingsPerMethod = bindingsPerMethod;
+ }
+ }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/registry/InterceptorRegistry.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/registry/InterceptorRegistry.java
new file mode 100644
index 0000000..accca67
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/registry/InterceptorRegistry.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.karaf.service.interceptor.impl.runtime.registry;
+
+import static java.util.stream.Collectors.toList;
+
+import java.lang.annotation.Annotation;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import org.apache.karaf.service.interceptor.api.InterceptorBinding;
+import org.apache.karaf.service.interceptor.impl.runtime.PropertiesManager;
+import org.apache.karaf.service.interceptor.impl.runtime.hook.InterceptorInstance;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+
+public class InterceptorRegistry implements ServiceListener {
+ private final Consumer<Class<?>> onAddition;
+ private final Consumer<Class<?>> onRemoval;
+ private final PropertiesManager propertiesManager;
+ private final Map<Class<?>, InterceptorInstance<?>> interceptors = new ConcurrentHashMap<>();
+
+ public InterceptorRegistry(final Consumer<Class<?>> onAddition,
+ final Consumer<Class<?>> onRemoval,
+ final PropertiesManager propertiesManager) {
+ this.onAddition = onAddition;
+ this.onRemoval = onRemoval;
+ this.propertiesManager = propertiesManager;
+ }
+
+ public boolean areBindingsAvailable(final Stream<Class<?>> bindings) {
+ return bindings.allMatch(binding -> binding != null && interceptors.containsKey(binding));
+ }
+
+ public List<InterceptorInstance<?>> getInterceptors(final List<Class<?>> bindings) {
+ return bindings.stream().map(interceptors::get).distinct().collect(toList());
+ }
+
+ @Override
+ public void serviceChanged(final ServiceEvent serviceEvent) {
+ final Class<? extends Annotation> bindingClass = getInterceptorBinding(serviceEvent);
+ switch (serviceEvent.getType()) {
+ case ServiceEvent.REGISTERED: {
+ interceptors.put(bindingClass, new InterceptorInstance<>(
+ serviceEvent.getServiceReference(), bindingClass, propertiesManager));
+ onAddition.accept(bindingClass);
+ break;
+ }
+ case ServiceEvent.MODIFIED_ENDMATCH:
+ case ServiceEvent.UNREGISTERING: {
+ interceptors.remove(bindingClass);
+ onRemoval.accept(bindingClass);
+ break;
+ }
+ case ServiceEvent.MODIFIED:
+ default:
+ }
+ }
+
+ private Class<? extends Annotation> getInterceptorBinding(final ServiceEvent serviceEvent) {
+ final List<Annotation> bindings = propertiesManager.unflattenStringValues(serviceEvent.getServiceReference().getProperty(Constants.OBJECTCLASS))
+ .map(it -> {
+ try {
+ return serviceEvent.getServiceReference().getBundle().loadClass(it);
+ } catch (final ClassNotFoundException e) {
+ throw new IllegalStateException(e);
+ }
+ })
+ .flatMap(it -> Stream.of(it.getAnnotations()))
+ .filter(it -> it.annotationType().isAnnotationPresent(InterceptorBinding.class))
+ .distinct()
+ .collect(toList());
+ if (bindings.size() != 1) {
+ throw new IllegalArgumentException("A single @InterceptorBinding on " + serviceEvent + " is required, found: " + bindings);
+ }
+ // todo: keep annotation instance to support binding values?
+ return bindings.iterator().next().annotationType();
+ }
+}
diff --git a/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/E2ETest.java b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/E2ETest.java
new file mode 100644
index 0000000..048422c
--- /dev/null
+++ b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/E2ETest.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.karaf.service.interceptor.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.ops4j.pax.exam.CoreOptions.bundle;
+import static org.ops4j.pax.exam.CoreOptions.systemTimeout;
+import static org.ops4j.pax.exam.CoreOptions.url;
+import static org.ops4j.pax.exam.container.remote.RBCRemoteTargetOptions.waitForRBCFor;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.configureConsole;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.configureSecurity;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.editConfigurationFilePut;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.features;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.karafDistributionConfiguration;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.keepRuntimeFolder;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.logLevel;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.apache.karaf.service.interceptor.impl.test.InterceptedService;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.ProbeBuilder;
+import org.ops4j.pax.exam.TestProbeBuilder;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.karaf.options.LogLevelOption;
+import org.ops4j.pax.exam.options.UrlProvisionOption;
+import org.ops4j.pax.exam.options.UrlReference;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+import org.osgi.framework.Constants;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class E2ETest {
+ @Inject
+ private InterceptedService interceptedService;
+
+ @Test
+ public void run() {
+ assertTrue(interceptedService.getClass().getName().contains("$$KarafInterceptorProxy"));
+ assertEquals("wrapped>from 'org.apache.karaf.service.interceptor.impl.test.InterceptedService'<", interceptedService.wrap());
+ assertEquals("wrapped>'bar'(suffixed)<", interceptedService.wrapAndSuffix("bar"));
+ }
+
+ @ProbeBuilder
+ public TestProbeBuilder probeConfiguration(final TestProbeBuilder probe) {
+ probe.setHeader(Constants.EXPORT_PACKAGE, "org.apache.karaf.service.interceptor.impl.test");
+ probe.setHeader("Service-Component",
+ "OSGI-INF/org.apache.karaf.service.interceptor.impl.test.InterceptedService.xml," +
+ "OSGI-INF/org.apache.karaf.service.interceptor.impl.test.SuffixingInterceptor.xml," +
+ "OSGI-INF/org.apache.karaf.service.interceptor.impl.test.WrappingInterceptor.xml");
+ return probe;
+ }
+
+ @Configuration
+ public Option[] config() throws MalformedURLException {
+ final String localRepository = System.getProperty("org.ops4j.pax.url.mvn.localRepository", "");
+ final UrlReference karafUrl = url(new File("target/libs/karaf.tar.gz").toURI().toURL().toExternalForm());
+ final UrlReference asmUrl = url(new File("target/libs/asm.jar").toURI().toURL().toExternalForm());
+ final UrlProvisionOption apiBundle = url(Optional.ofNullable(new File("../api/target")
+ .listFiles((dir, name) -> name.startsWith("org.apache.karaf.services.interceptor.api-") && isNotReleaseArtifact(name)))
+ .map(files -> files[0])
+ .orElseThrow(() -> new IllegalArgumentException("No interceptor api bundle found, ensure api module was built"))
+ .toURI().toURL().toExternalForm());
+ final UrlProvisionOption implBundle = url(Optional.ofNullable(new File("target")
+ .listFiles((dir, name) -> name.startsWith("org.apache.karaf.services.interceptor.impl-") && isNotReleaseArtifact(name)))
+ .map(files -> files[0])
+ .orElseThrow(() -> new IllegalArgumentException("No interceptor impl bundle found, ensure impl module was built"))
+ .toURI().toURL().toExternalForm());
+ return new Option[]{
+ karafDistributionConfiguration()
+ .frameworkUrl(karafUrl.getURL())
+ .name("Apache Karaf")
+ .runEmbedded(true)
+ .unpackDirectory(new File("target/exam")),
+ configureSecurity().disableKarafMBeanServerBuilder(),
+ configureConsole().ignoreLocalConsole(),
+ keepRuntimeFolder(),
+ logLevel(LogLevelOption.LogLevel.INFO),
+ systemTimeout(3600000),
+ waitForRBCFor(3600000),
+ editConfigurationFilePut("etc/org.apache.karaf.features.cfg", "updateSnapshots", "none"),
+ editConfigurationFilePut("etc/org.ops4j.pax.url.mvn.cfg", "org.ops4j.pax.url.mvn.localRepository", localRepository),
+ editConfigurationFilePut("etc/branding.properties", "welcome", ""), // No welcome banner
+ editConfigurationFilePut("etc/branding-ssh.properties", "welcome", ""),
+ features("mvn:org.apache.karaf.features/standard/" + System.getProperty("karaf.version") + "/xml/features", "scr"),
+ bundle(asmUrl.getURL()),
+ bundle(apiBundle.getURL()),
+ bundle(implBundle.getURL())
+ };
+ }
+
+ private boolean isNotReleaseArtifact(final String name) {
+ return name.endsWith(".jar") && !name.contains("-sources") && !name.contains("javadoc");
+ }
+}
diff --git a/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/AsmProxyFactoryTest.java b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/AsmProxyFactoryTest.java
new file mode 100644
index 0000000..03bcc3f
--- /dev/null
+++ b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/AsmProxyFactoryTest.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.karaf.service.interceptor.impl.runtime.proxy;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+public class AsmProxyFactoryTest {
+ @Test
+ public void proxy() {
+ final ProxyFactory.ProxyClassLoader classLoader = new ProxyFactory.ProxyClassLoader(Thread.currentThread().getContextClassLoader(), null);
+ final AsmProxyFactory factory = new AsmProxyFactory();
+ final Class<?> proxyClass = factory.createProxyClass(
+ classLoader, Foo.class.getName() + "$$ProxyTestProxy1",
+ new Class<?>[]{Foo.class},
+ Foo.class.getDeclaredMethods());
+ assertNotNull(proxyClass);
+
+ final Foo instance = Foo.class.cast(factory.create(proxyClass, (method, args) -> {
+ switch (method.getName()) {
+ case "fail":
+ throw new IOException("it must be a checked exception to ensure it is well propagated");
+ default:
+ return method.getName() + "(" + asList(args) + ")";
+ }
+ }));
+ assertEquals("foo1([])", instance.foo1());
+ assertEquals("foo2([param])", instance.foo2("param"));
+ assertTrue(instance.toString().startsWith(Foo.class.getName() + "$$ProxyTestProxy1@"));
+ try {
+ instance.fail();
+ fail();
+ } catch (final IOException e) {
+ assertEquals("it must be a checked exception to ensure it is well propagated", e.getMessage());
+ }
+ }
+
+ public static class Foo {
+ public String foo1() {
+ return "first";
+ }
+
+ public String foo2(final String some) {
+ return "second<" + some + ">";
+ }
+
+ public String fail() throws IOException {
+ return "ok";
+ }
+ }
+}
diff --git a/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/InterceptedService.java b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/InterceptedService.java
new file mode 100644
index 0000000..6d5eaec
--- /dev/null
+++ b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/InterceptedService.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.karaf.service.interceptor.impl.test;
+
+import org.apache.karaf.service.interceptor.api.EnableInterceptors;
+import org.osgi.service.component.annotations.Component;
+
+@Wrap
+@EnableInterceptors
+@Component(service = InterceptedService.class)
+public class InterceptedService {
+ @Suffix
+ public String wrapAndSuffix(final String value) {
+ return "'" + value + "'";
+ }
+
+ public String wrap() {
+ return "from '" + getClass().getName() + "'";
+ }
+}
diff --git a/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/Suffix.java b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/Suffix.java
new file mode 100644
index 0000000..d62b735
--- /dev/null
+++ b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/Suffix.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.karaf.service.interceptor.impl.test;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.apache.karaf.service.interceptor.api.InterceptorBinding;
+
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+@InterceptorBinding
+public @interface Suffix {
+}
diff --git a/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/SuffixingInterceptor.java b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/SuffixingInterceptor.java
new file mode 100644
index 0000000..4418a2e
--- /dev/null
+++ b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/SuffixingInterceptor.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.karaf.service.interceptor.impl.test;
+
+import org.apache.karaf.service.interceptor.api.AroundInvoke;
+import org.apache.karaf.service.interceptor.api.Interceptor;
+import org.apache.karaf.service.interceptor.api.InvocationContext;
+import org.osgi.service.component.annotations.Component;
+
+@Suffix
+@Interceptor
+@Component(service = SuffixingInterceptor.class)
+public class SuffixingInterceptor {
+ @AroundInvoke
+ public Object around(final InvocationContext context) throws Exception {
+ return context.proceed() + "(suffixed)";
+ }
+}
diff --git a/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/Wrap.java b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/Wrap.java
new file mode 100644
index 0000000..6280fdf
--- /dev/null
+++ b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/Wrap.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.karaf.service.interceptor.impl.test;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.apache.karaf.service.interceptor.api.InterceptorBinding;
+
+@Target(TYPE)
+@Retention(RUNTIME)
+@InterceptorBinding
+public @interface Wrap {
+}
diff --git a/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/WrappingInterceptor.java b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/WrappingInterceptor.java
new file mode 100644
index 0000000..0653c0e
--- /dev/null
+++ b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/WrappingInterceptor.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.karaf.service.interceptor.impl.test;
+
+import org.apache.karaf.service.interceptor.api.AroundInvoke;
+import org.apache.karaf.service.interceptor.api.Interceptor;
+import org.apache.karaf.service.interceptor.api.InvocationContext;
+import org.osgi.service.component.annotations.Component;
+
+@Wrap
+@Interceptor
+@Component(service = WrappingInterceptor.class)
+public class WrappingInterceptor {
+ @AroundInvoke
+ public Object around(final InvocationContext context) throws Exception {
+ return "wrapped>" + context.proceed() + "<";
+ }
+}
diff --git a/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.InterceptedService.xml b/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.InterceptedService.xml
new file mode 100644
index 0000000..919a970
--- /dev/null
+++ b/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.InterceptedService.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.3.0" name="org.apache.karaf.service.interceptor.impl.test.InterceptedService">
+ <property name="apache.karaf.enable.interceptors" type="Boolean" value="true"/>
+ <implementation class="org.apache.karaf.service.interceptor.impl.test.InterceptedService" />
+ <service>
+ <scr:provide interface="org.apache.karaf.service.interceptor.impl.test.InterceptedService"/>
+ </service>
+</scr:component>
diff --git a/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.SuffixingInterceptor.xml b/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.SuffixingInterceptor.xml
new file mode 100644
index 0000000..89d057e
--- /dev/null
+++ b/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.SuffixingInterceptor.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.3.0" name="org.apache.karaf.service.interceptor.impl.test.SuffixingInterceptor">
+ <property name="apache.karaf.interceptor" type="Boolean" value="true"/>
+ <implementation class="org.apache.karaf.service.interceptor.impl.test.SuffixingInterceptor"/>
+ <service>
+ <scr:provide interface="org.apache.karaf.service.interceptor.impl.test.SuffixingInterceptor"/>
+ </service>
+</scr:component>
diff --git a/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.WrappingInterceptor.xml b/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.WrappingInterceptor.xml
new file mode 100644
index 0000000..47e8071
--- /dev/null
+++ b/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.WrappingInterceptor.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.3.0" name="org.apache.karaf.service.interceptor.impl.test.WrappingInterceptor">
+ <property name="apache.karaf.interceptor" type="Boolean" value="true"/>
+ <implementation class="org.apache.karaf.service.interceptor.impl.test.WrappingInterceptor"/>
+ <service>
+ <scr:provide interface="org.apache.karaf.service.interceptor.impl.test.WrappingInterceptor"/>
+ </service>
+</scr:component>
diff --git a/services/interceptor/pom.xml b/services/interceptor/pom.xml
new file mode 100644
index 0000000..71035e0
--- /dev/null
+++ b/services/interceptor/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.karaf.services</groupId>
+ <artifactId>services</artifactId>
+ <version>4.3.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>org.apache.karaf.services.interceptor</artifactId>
+ <packaging>pom</packaging>
+ <name>Apache Karaf :: Services :: Interceptor</name>
+ <description>Interceptor support (inspired from JavaEE/JakartaEE) on top of SCR.</description>
+
+ <modules>
+ <module>api</module>
+ <module>impl</module>
+ </modules>
+</project>
diff --git a/services/pom.xml b/services/pom.xml
index 6916d86..228fad6 100644
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -37,6 +37,7 @@
<module>coordinator</module>
<module>eventadmin</module>
<module>staticcm</module>
+ <module>interceptor</module>
</modules>
</project>