osgi: Using ServiceFactories?

Marc-Christian Schulze picture Marc-Christian Schulze · Aug 11, 2011 · Viewed 11.8k times · Source

I'm currently trying to get a simple bundle containing a Service Factory running.

This is my factory class:

public class SvcFactory implements ServiceFactory<ServiceB> {

    @Override
    public ServiceB getService(Bundle bundle,
            ServiceRegistration<ServiceB> registration) {

        return new ServiceBImpl();
    }

    @Override
    public void ungetService(Bundle bundle, ServiceRegistration<ServiceB> registration,
            ServiceB service) {

    }

}

This is my service that should be created by the factory:

public class ServiceBImpl implements ServiceB {

    private ServiceA svcA;

    public void setA(ServiceA a) {
        svcA = a;
    }

}

And finally the OSGI-INF/component.xml

<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="bundleb.internal.SvcFactory">

   <implementation class="bundleb.internal.SvcFactory"/>

  <reference bind="setA" cardinality="1..1" interface="bundlea.ServiceA" name="ServiceA" policy="static"/>

   <service servicefactory="true">
      <provide interface="bundleb.ServiceB"/>
   </service>
</scr:component>

If I run my test bundles (A, B and C) within equinox I'm getting the following error:

org.osgi.framework.ServiceException: org.eclipse.equinox.internal.ds.FactoryReg.getService() returned a service object that is not an instance of the service class bundleb.ServiceB

I can't find much information about using ServiceFeactories declared in a component definition on the internet. Even the book "OSGi and Equinox" didn't tell me much about using them. Could anyone please explain to me what I'm doing wrong?

Answer

earcam picture earcam · Aug 16, 2011

Here's an example using ComponentFactory which should fit your needs (and contains a simple integration test to aid with your other question). Disclaimer; the code isn't well written, just for example's sake.

Some service interfaces:

package net.earcam.example.servicecomponent;

public interface EchoService {

    String REPEAT_PARAMETER = "repeat";
    String FACTORY_DS = "echo.factory";
    String NAME_DS = "echo";

    String echo(String message);
}

And:

package net.earcam.example.servicecomponent;

public interface SequenceService {
    long next();
}

Then the implementations:

import static net.earcam.example.servicecomponent.EchoService.FACTORY_DS;
import static net.earcam.example.servicecomponent.EchoService.NAME_DS;
import static org.apache.felix.scr.annotations.ReferenceCardinality.MANDATORY_UNARY;
import static org.apache.felix.scr.annotations.ReferencePolicy.DYNAMIC;
import net.earcam.example.servicecomponent.EchoService;
import net.earcam.example.servicecomponent.SequenceService;

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.osgi.service.component.ComponentContext;

@Component(factory = FACTORY_DS, name = NAME_DS)
public class EchoServiceImp implements EchoService {

    @Reference(cardinality = MANDATORY_UNARY, policy = DYNAMIC)
    private SequenceService sequencer = null;
    private transient int repeat = 1;

    @Activate
protected void activate(final ComponentContext componentContext)
{
    repeat = Integer.parseInt(componentContext.getProperties().get(REPEAT_PARAMETER).toString());
}


@Override
public String echo(final String message)
{
    StringBuilder stringBuilder = new StringBuilder();
    for(int i = 0; i < repeat; i++) {
        addEchoElement(stringBuilder, message);
    }
    return stringBuilder.toString();
}


private void addEchoElement(final StringBuilder stringBuilder, final String message) {
    stringBuilder.append(sequencer.next()).append(' ').append(message).append("\n");        
}


protected void unbindSequencer()
{
    sequencer = null;
}


protected void bindSequencer(final SequenceService sequencer)
{
    this.sequencer = sequencer;
}

}

And:

package net.earcam.example.servicecomponent.internal;

import java.util.concurrent.atomic.AtomicLong;

import net.earcam.example.servicecomponent.SequenceService;

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Service;

/**
 * @author caspar
 */
@Component
@Service
public class SequenceServiceImp implements SequenceService {

    private AtomicLong sequence;


    @Override
    public long next()
    {
        return sequence.incrementAndGet();
    }


    @Activate
    protected void activate()
    {
        sequence = new AtomicLong();
    }


    @Deactivate
    protected void deactivate()
    {
        sequence = null;
    }
}

An integration test that drives the whole thing (note; there's a main method so you run it while start/stopping bundles etc).

package net.earcam.example.servicecomponent.test;

import static org.ops4j.pax.exam.CoreOptions.*;
import static org.ops4j.pax.exam.OptionUtils.combine;
import static org.ops4j.pax.exam.spi.container.PaxExamRuntime.createContainer;
import static org.ops4j.pax.exam.spi.container.PaxExamRuntime.createTestSystem;

import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.earcam.example.servicecomponent.EchoService;
import net.earcam.example.servicecomponent.SequenceService;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.ops4j.pax.exam.Option;
import org.ops4j.pax.exam.junit.Configuration;
import org.ops4j.pax.exam.junit.ExamReactorStrategy;
import org.ops4j.pax.exam.junit.JUnit4TestRunner;
import org.ops4j.pax.exam.spi.reactors.EagerSingleStagedReactorFactory;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentFactory;
import org.osgi.service.component.ComponentInstance;


@ExamReactorStrategy(EagerSingleStagedReactorFactory.class)
@RunWith(JUnit4TestRunner.class)
public class EchoServiceIntegrationTest {


    public static void main(String[] args) {
        try {
            createContainer(
                    createTestSystem(
                            combine(
                                    new EchoServiceIntegrationTest().config(), 
                                    profile("gogo"))
                    )).start();
        } catch(Throwable t) {
            t.printStackTrace();
        }
    }



    @Configuration
    public Option[] config()
    {
        return options(
                felix(),
                equinox(),
                junitBundles(),
                systemProperty("org.ops4j.pax.logging.DefaultServiceLog.level").value("TRACE"),
                mavenBundle().groupId("org.apache.felix").artifactId("org.apache.felix.scr").versionAsInProject(),
                bundle("file:" + findFileInCurrentDirectoryAndBelow(
                        Pattern.compile("net\\.earcam\\.example\\.servicecomponent\\-[\\.\\d]+(\\-SNAPSHOT)?\\.[wj]ar")))
        );
    }


    @Test
    public void bundleContextIsAvailable(BundleContext context)
    {
        Assert.assertNotNull("PAX Exam BundleContext available", context);
    }


    @Test
    public void sequenceServiceIsAvailable(BundleContext context)
    {
        Assert.assertNotNull("SequenceService available", fetchService(context, SequenceService.class));
    }


    @Test
    public void serviceResponseContainsThreeEchos(BundleContext context) throws Exception
    {
        final String message = "message";
        final String expected = "1 " + message + "\n2 " + message + "\n3 " + message + "\n";

        ComponentFactory factory = fetchComponentFactory(context, EchoService.FACTORY_DS);

        Dictionary<String, String> properties = new Hashtable<String, String>();
        properties.put(EchoService.REPEAT_PARAMETER, "3");
        ComponentInstance instance = factory.newInstance(properties);
        EchoService service = (EchoService) instance.getInstance();
        String actual = service.echo(message);
        Assert.assertEquals("Expected response", expected, actual);
    }


    private ComponentFactory fetchComponentFactory(BundleContext context, String componentFactoryId) throws Exception
    {
        String filter = "(component.factory=" + componentFactoryId + ")";
        ServiceReference[] references = context.getServiceReferences(ComponentFactory.class.getCanonicalName(), filter);
        return (references.length) == 0 ?  null : (ComponentFactory) context.getService(references[0]);
    }


    private <T> T fetchService(BundleContext context, Class<T> clazz)
    {
        ServiceReference reference = context.getServiceReference(clazz.getCanonicalName());
        @SuppressWarnings("unchecked")
        T service = (T) context.getService(reference);
        return service;
    }


    private String findFileInCurrentDirectoryAndBelow(final Pattern filePattern) {
        FileFilter filter = new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                Matcher matcher = filePattern.matcher(pathname.getName());
                return (matcher.matches());
            }
        };
        return findFile(new File("."), filter, filePattern);
    }


    private String findFile(File directory, FileFilter filter, Pattern filePattern) {
        File[] matches = directory.listFiles(filter);
        if(matches != null && matches.length > 0) {
            return matches[0].getAbsolutePath();
        }
        File[] subdirs = directory.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.isDirectory();
            }
        });
        for(final File subdir : subdirs) {
            String found = findFile(subdir, filter, filePattern);
            if(!"".equals(found)) {
                return found;
            }
        }
        throw new RuntimeException(new FileNotFoundException("No match for pattern: " + filePattern.pattern()));
    }
}

And here's the maven pom:

<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">
    <modelVersion>4.0.0</modelVersion>
    <groupId>net.earcam</groupId>
    <artifactId>net.earcam.example.servicecomponent</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <version.java.source>1.6</version.java.source>
        <version.java.target>1.6</version.java.target>

        <version.osgi>4.2.0</version.osgi>
        <version.paxexam>2.1.0</version.paxexam>
        <version.paxrunner>1.7.4</version.paxrunner>
        <version.cometd>2.3.1</version.cometd>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.osgi</groupId>
            <artifactId>org.osgi.core</artifactId>
            <version>${version.osgi}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.osgi</groupId>
            <artifactId>org.osgi.compendium</artifactId>
            <version>${version.osgi}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.felix</groupId>
            <artifactId>org.apache.felix.scr.annotations</artifactId>
            <version>1.4.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.8.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
            <version>1.3.RC2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.jmock</groupId>
            <artifactId>jmock-junit4</artifactId>
            <version>2.5.1</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.6.1</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.ops4j.pax.exam</groupId>
            <artifactId>pax-exam-junit4</artifactId>
            <version>${version.paxexam}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.ops4j.pax.exam</groupId>
            <artifactId>pax-exam-container-paxrunner</artifactId>
            <version>${version.paxexam}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.ops4j.pax.exam</groupId>
            <artifactId>pax-exam-link-assembly</artifactId>
            <version>${version.paxexam}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.ops4j.pax.exam</groupId>
            <artifactId>pax-exam-testforge</artifactId>
            <version>${version.paxexam}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.ops4j.pax.runner</groupId>
            <artifactId>pax-runner-no-jcl</artifactId>
            <version>${version.paxrunner}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.felix</groupId>
            <artifactId>org.apache.felix.scr</artifactId>
            <version>1.6.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>${version.java.source}</source>
                    <target>${version.java.target}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>

            <plugin>
                <!-- Unit Tests -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.8.1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>test</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <excludes>
                        <exclude>**/*IntegrationTest.java</exclude>
                    </excludes>
                </configuration>
            </plugin>

            <plugin>
                <!-- Integration Tests -->
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>failsafe-maven-plugin</artifactId>
                <version>2.4.3-alpha-1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                        <phase>integration-test</phase>
                    </execution>
                </executions>
                <configuration>
                    <includes>
                        <include>**/*IntegrationTest.java</include>
                    </includes>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.ops4j.pax.exam</groupId>
                <artifactId>maven-paxexam-plugin</artifactId>
                <version>1.2.3</version>
                <executions>
                    <execution>
                        <id>generate-config</id>
                        <goals>
                            <goal>generate-depends-file</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <!-- Process the DS annotations -->
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-scr-plugin</artifactId>
                <version>1.6.0</version>
                <executions>
                    <execution>
                        <id>generate-scr-descriptor</id>
                        <goals>
                            <goal>scr</goal>
                        </goals>
                        <phase>process-classes</phase>
                        <configuration>
                            <strictMode>true</strictMode>
                            <outputDirectory>${project.build.outputDirectory}/</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>


            <plugin>
                <!-- Generate OSGi bundle MAINFEST.MF entries -->
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <version>2.3.4</version>
                <extensions>true</extensions>
                <configuration>
                    <supportedProjectTypes>
                        <supportedProjectType>jar</supportedProjectType>
                    </supportedProjectTypes>
                    <instructions>
                        <Bundle-Vendor>earcam</Bundle-Vendor>
                        <Service-Component>OSGI-INF/serviceComponents.xml</Service-Component>
                        <!-- PAX mangles this, it uses the name of the project for the symbolicname 
                            of test bundle? <Bundle-SymbolicName>${project.name}</Bundle-SymbolicName> -->
                        <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
                        <Bundle-Version>${project.version}</Bundle-Version>
                        <Export-Package>!${project.artifactId}.internal,${project.artifactId}.*</Export-Package>
                        <Import-Package>*</Import-Package>
                    </instructions>
                </configuration>
                <executions>
                    <execution>
                        <id>bundle-manifest</id>
                        <phase>process-classes</phase>
                        <goals>
                            <goal>manifest</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.3.1</version>
                <configuration>
                    <archive>
                        <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                </configuration>
            </plugin>

        </plugins>
    </build>
</project>

A couple of things to note; I like my integration tests inside the module they test, that way mvn clean install deploy fails if my integration test does - but it's common to see projects with a single integration module for all integration tests. This explains the ugly method findFileInCurrentDirectoryAndBelow(Pattern pattern) which is used to locate the current module's bundle in the target directory, and also explains the non-standard setup of the maven-bundle-plugin and maven-scr-plugin plugins.

Also the way Pax-Exam picks up the dependencies requires you run the maven build for every change in dependencies and config (e.g. bundle imports/exports, DS changes). But once this is done you can run/debug the tests from Eclipse.

I've put the project in a tarball here

HTH =)