3

I'm new to OSGi and am quickly becoming overwhelmed by its complexity. I believe this should be fairly simple, but I haven't been able to find a complete working example of what I'm trying to achieve.

I have a Java class Foo which contains a collection of services. These services need to be filtered based on a value which is specific to that particular instance of Foo. There can be multiple instances of Foo, but each one should have its own set of filtered services.

To illustrate, consider the following example (inspired by the Apache Felix tutorials):

public interface DictionaryService {
    public boolean check(String word);
}
@Component(property = "language=en")
public class EnglishDictionaryService implements DictionaryService {
    private static final String[] WORDS = {"hi", "hello" /*...*/};

    @Override
    public boolean check(String word) {
        if (word == null || word.isEmpty()) {
            return true;
        }

        // super inefficient but you get the gist
        return Arrays.stream(WORDS).anyMatch(entry -> word.equalsIgnoreCase(entry));
    }
}
@Component(property = "language=en")
public class TexanDictionaryService implements DictionaryService {
    private static final String[] WORDS = {"howdy" /*...*/};
    //...
}
@Component(property = "language=en")
public class AustralianDictionaryService implements DictionaryService {
    private static final String[] WORDS = {"g'day" /*...*/};
    //...
}
@Component(property = "language=es")
public class SpanishDictionaryService implements DictionaryService {
    private static final String[] WORDS = {"hola" /*...*/};
    //...
}
@Component
public class SpellChecker {

    @Reference
    private volatile List<DictionaryService> dictionaryServices;

    public SpellChecker(String language) {
        // TODO: how to ensure my dictionaryServices match the given language code?
        // dictionaryServices.target = "(language=" + language + ")"
    }

    public boolean check(String word) {
        if (word == null || word.isEmpty()) {
            return true;
        }

        List<DictionaryService> ds = dictionaryServices;

        if (ds == null || ds.isEmpty()) {
            return false;
        }

        return ds.stream().anyMatch(dictionary -> dictionary.check(word));
    }
}
public static void main(String[] args) {
    SpellChecker englishChecker = new SpellChecker("en");
    SpellChecker spanishChecker = new SpellChecker("es");
    // do stuff
}

After reading through several StackExchange posts and some other articles, it seems this can be done using ConfigurationAdmin. However, it's unclear exactly where and how ConfigurationAdmin should be used, especially with regards to Declarative Services. I've also read and reread the Configuration Admin Service Specification, but I'm struggling to apply the concepts.

Can someone fill in the gaps in my understanding?

Thanks in advance!


Edit

Christian's answer helped me think about Declarative Services in a different way. As I was going back through my research, I came across Alan Hohn's blog posts on DZone again. Unfortunately, it seems he never finished his series which promised to cover service lookups using DS. However, his example source code contains the following:

public String greet(String language) {
    BundleContext context = FrameworkUtil.getBundle(this.getClass()).getBundleContext();

    String filter = "(language=" + language + ")";

    // Get ServiceReference(s) from OSGi framework, filtered for the specified value
    ServiceReference[] refs = null;
    try {
        refs = context.getServiceReferences(Greeter.class.getName(), filter);
        if (null != refs) {
            Greeter greeter = (Greeter)context.getService(refs[0]);
            return greeter.greet();
        }
    } catch (InvalidSyntaxException e) {
        LOGGER.error("Invalid query syntax", e);
    }
    LOGGER.warn("No plugins found, making the default greeting");
    return "Hello from the greeter manager!";
}

This looks like a viable solution, but it doesn't appear to use DS. Are there any special considerations with this approach? I've seen plenty of posts on SO and elsewhere which claim DS is the cure-all for BundleContext#getServiceReferences, so I'm curious if/how this could be refactored to use DS.

roadkill
  • 315
  • 1
  • 2
  • 12

3 Answers3

1

Your code in main does not make sense.

If you create the instance of a (declarative services) DS component with the new keyword then the whole DS logic will not be executed. Actually in OSGi you do not use a main method at all... maybe for starting the framework but not for your own logic.

You can access your spell checker by creating a shell command that uses it or by creating an http whiteboard service that uses it.

For setting the filter for the service references in SpellChecker you could use a configuration like:

pid: fully qualified name of SpellChecker


dictionaryServices.target=(language=en)

This would set the SpellChecker to use only the English dictionaries.

For some more tips about DS you can refer to https://liquid-reality.de/2016/09/26/hints-ds.html

lealceldeiro
  • 14,342
  • 6
  • 49
  • 80
Christian Schneider
  • 19,420
  • 2
  • 39
  • 64
  • In reality I'm not using a main method, but `If you create the instance of a (declarative services) DS component with "new" then the whole DS logic will not be executed` could explain a fair bit of my confusion. – roadkill Apr 12 '19 at 14:47
  • Thanks for setting me on the right path. What are your thoughts on my edit? – roadkill Apr 15 '19 at 13:37
0

As I understand, you want to have 1:N relationship between two components.

With DS, you have a couple of alternatives:

whiteboard pattern

You can implement whiteboard pattern, where component 1 tracks service registrations of the DictionaryService OSGi services. Component N registers services and each service registrations are caught and used by component 1 that are registered.

Problem might be that you do not want to activate component 1 in production until all expected component N registered their services and tracked by 1.

Use a multi-cardinality reference with a complex filter expression

You use a reference with multiple cardinality and a filter expression in the configuration like: (|(language=en)(language=es))

Problem is the same as with whiteboard pattern.

Many people start writing a "health checker" where 1:N relationship is also defined and notifies the programmer if not all services are started (or disallows the app to be accessible for the user). Problem with health checker approach is that programmer has to have the same logic redundant in the system.

Instead of DS, use ECM (another component model for OSGi)

While DS multi-cardinality reference and whiteboard pattern give a very comfortable flexibility in development time, it is often not suitable for production when all services must be injected before the application becomes accessible by the users.

ECM supports 1:N relationship in the following way:

  • In component 1 you can define an array of filters
  • Component 1 will be satisfied only when there is an OSGi service for all specified filters.

As the scope and goal of an ECM component are pretty similar to a DS component's scope and goal, people knowing DS need only a couple of hours to learn ECM. As ECM is also dependent on OSGi services, DS and ECM can live easily next to each other in the same system and use the OSGi services provided by the other one.

Based on your example:

// All annotations from the ecm package

@Component
public class SpellChecker {

  @ServiceRef
  private DictionaryService[] dictionaryServices;

  // I think the language should be a parameter of your service function
  // rather than a member variable of your component class
  public boolean check(String word, String language) {
    if (word == null || word.isEmpty()) {
      return true;
    }

    if (dictionaryServices == null || dictionaryService.length = 0) {
      return false;
    }

    List<DictionaryService> ds = Arrays.asList(dictionaryServices);

    return ds.stream().anyMatch(dictionary -> dictionary.check(word));
  }

  // You need a setter in case of ECM and you can annotate the setter as well.
  // If you annotate the field instead, you need to specify the setter as an
  // attribute of the annotation
  @ServiceRef()
  public void setDictionaryServices(DictionaryService[] dictionaryServices) {
    this.dictionaryServices = dictionaryServices;
  }
}

The component above can be used with the following string array in the configuration:

dictionaryServices.target = [
                             "(language=en)",
                             "(language=de)",
                             "(language="es")"
                            ]

The component will be activated when all three references are available and you will get an array of dictionary services with three items in it (with the same order in the array as in the configuration).

Balazs Zsoldos
  • 6,036
  • 2
  • 23
  • 31
0

As I understand you want to fetch all service instance references of Dictionary Services which implements DictionaryService interface. You can achieve this by using below simple OSGI DS code.

@Reference(cardinality = ReferenceCardinality.MULTIPLE)
private volatile List<DictionaryService> dictionaryServices;